diff --git a/locales/index.d.ts b/locales/index.d.ts index c31a3f4e83..2040d55628 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12548,6 +12548,20 @@ export interface Locale extends ILocale { */ "listDrafts": string; }; + /** + * 二次元コード + */ + "qr": string; + "_qr": { + /** + * 表示 + */ + "showTabTitle": string; + /** + * 読み取る + */ + "readTabTitle": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 522f53ce4d..cf26446a4d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3359,3 +3359,8 @@ _drafts: restoreFromDraft: "下書きから復元" restore: "復元" listDrafts: "下書き一覧" + +qr: 二次元コード +_qr: + showTabTitle: "表示" + readTabTitle: "読み取る" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 952af75d97..682429baad 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -63,6 +63,7 @@ "misskey-reversi": "workspace:*", "photoswipe": "5.4.4", "punycode.js": "2.3.1", + "qr-code-styling": "1.9.2", "rollup": "4.48.0", "sanitize-html": "2.17.0", "sass": "1.90.0", diff --git a/packages/frontend/src/directives/flip-on-device-orientation.ts b/packages/frontend/src/directives/flip-on-device-orientation.ts new file mode 100644 index 0000000000..767d42a480 --- /dev/null +++ b/packages/frontend/src/directives/flip-on-device-orientation.ts @@ -0,0 +1,44 @@ +import type { Directive } from 'vue'; + +let initialized = false; +let styleEl: HTMLStyleElement | null = null; +const elements = new Set(); +const className = '_flipOnDeviceOrientation'; +const variableName = `--flip_on_device_orientation_transform`; + +function handleOrientationChange() { + const isUpsideDown = window.screen.orientation.type === 'landscape-secondary'; + const transform = isUpsideDown ? 'scale(-1, -1)' : ''; + window.document.body.style.setProperty(variableName, transform); +} + +function registerListener() { + if (!initialized) { + screen.orientation.addEventListener('change', handleOrientationChange); + if (!styleEl) { + styleEl = window.document.createElement('style'); + styleEl.textContent = `.${className} { transform: var(${variableName}); }`; + window.document.head.appendChild(styleEl); + } + initialized = true; + } else if (window.document.getElementsByClassName(className).length === 0) { + screen.orientation.removeEventListener('change', handleOrientationChange); + if (styleEl) { + window.document.head.removeChild(styleEl); + styleEl = null; + } + initialized = false; + } +} + +export default { + mounted(el) { + registerListener(); + el.classList.add(className); + handleOrientationChange(); + }, + unmounted(el) { + el.classList.remove(className); + registerListener(); + }, +} as Directive; diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index 9555045afe..c6948d8c06 100644 --- a/packages/frontend/src/directives/index.ts +++ b/packages/frontend/src/directives/index.ts @@ -16,6 +16,7 @@ import clickAnime from './click-anime.js'; import panel from './panel.js'; import adaptiveBorder from './adaptive-border.js'; import adaptiveBg from './adaptive-bg.js'; +import flipOnDeviceOrientation from './flip-on-device-orientation.js'; export default function(app: App) { for (const [key, value] of Object.entries(directives)) { @@ -36,4 +37,5 @@ export const directives = { 'panel': panel, 'adaptive-border': adaptiveBorder, 'adaptive-bg': adaptiveBg, + 'flip-on-device-orientation': flipOnDeviceOrientation, }; diff --git a/packages/frontend/src/pages/qr.read.vue b/packages/frontend/src/pages/qr.read.vue new file mode 100644 index 0000000000..5cf8c6dee1 --- /dev/null +++ b/packages/frontend/src/pages/qr.read.vue @@ -0,0 +1,21 @@ + + + + + + + diff --git a/packages/frontend/src/pages/qr.show.vue b/packages/frontend/src/pages/qr.show.vue new file mode 100644 index 0000000000..75544c0ce1 --- /dev/null +++ b/packages/frontend/src/pages/qr.show.vue @@ -0,0 +1,179 @@ + + + + + + + diff --git a/packages/frontend/src/pages/qr.vue b/packages/frontend/src/pages/qr.vue new file mode 100644 index 0000000000..123534ac2b --- /dev/null +++ b/packages/frontend/src/pages/qr.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index e25e0fe161..d59c9d1c6f 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -590,6 +590,10 @@ export const ROUTE_DEF = [{ path: '/reversi/g/:gameId', component: page(() => import('@/pages/reversi/game.vue')), loginRequired: false, +}, { + path: '/qr', + component: page(() => import('@/pages/qr.vue')), + loginRequired: true, }, { path: '/debug', component: page(() => import('@/pages/debug.vue')), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66f42b70e3..40c660ce0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -844,6 +844,9 @@ importers: punycode.js: specifier: 2.3.1 version: 2.3.1 + qr-code-styling: + specifier: 1.9.2 + version: 1.9.2 rollup: specifier: 4.48.0 version: 4.48.0 @@ -9508,6 +9511,13 @@ packages: resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} engines: {node: '>=6.0.0'} + qr-code-styling@1.9.2: + resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==} + engines: {node: '>=18.18.0'} + + qrcode-generator@1.5.2: + resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} + qrcode@1.5.4: resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} engines: {node: '>=10.13.0'} @@ -21080,6 +21090,12 @@ snapshots: pvutils@1.1.3: {} + qr-code-styling@1.9.2: + dependencies: + qrcode-generator: 1.5.2 + + qrcode-generator@1.5.2: {} + qrcode@1.5.4: dependencies: dijkstrajs: 1.0.2