wip (qr.show.vue)
This commit is contained in:
parent
47d83e8930
commit
32b21b1186
|
@ -12548,6 +12548,20 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"listDrafts": string;
|
"listDrafts": string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 二次元コード
|
||||||
|
*/
|
||||||
|
"qr": string;
|
||||||
|
"_qr": {
|
||||||
|
/**
|
||||||
|
* 表示
|
||||||
|
*/
|
||||||
|
"showTabTitle": string;
|
||||||
|
/**
|
||||||
|
* 読み取る
|
||||||
|
*/
|
||||||
|
"readTabTitle": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -3359,3 +3359,8 @@ _drafts:
|
||||||
restoreFromDraft: "下書きから復元"
|
restoreFromDraft: "下書きから復元"
|
||||||
restore: "復元"
|
restore: "復元"
|
||||||
listDrafts: "下書き一覧"
|
listDrafts: "下書き一覧"
|
||||||
|
|
||||||
|
qr: 二次元コード
|
||||||
|
_qr:
|
||||||
|
showTabTitle: "表示"
|
||||||
|
readTabTitle: "読み取る"
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
|
"qr-code-styling": "1.9.2",
|
||||||
"rollup": "4.48.0",
|
"rollup": "4.48.0",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sass": "1.90.0",
|
"sass": "1.90.0",
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { Directive } from 'vue';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let styleEl: HTMLStyleElement | null = null;
|
||||||
|
const elements = new Set<HTMLElement>();
|
||||||
|
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;
|
|
@ -16,6 +16,7 @@ import clickAnime from './click-anime.js';
|
||||||
import panel from './panel.js';
|
import panel from './panel.js';
|
||||||
import adaptiveBorder from './adaptive-border.js';
|
import adaptiveBorder from './adaptive-border.js';
|
||||||
import adaptiveBg from './adaptive-bg.js';
|
import adaptiveBg from './adaptive-bg.js';
|
||||||
|
import flipOnDeviceOrientation from './flip-on-device-orientation.js';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
for (const [key, value] of Object.entries(directives)) {
|
for (const [key, value] of Object.entries(directives)) {
|
||||||
|
@ -36,4 +37,5 @@ export const directives = {
|
||||||
'panel': panel,
|
'panel': panel,
|
||||||
'adaptive-border': adaptiveBorder,
|
'adaptive-border': adaptiveBorder,
|
||||||
'adaptive-bg': adaptiveBg,
|
'adaptive-bg': adaptiveBg,
|
||||||
|
'flip-on-device-orientation': flipOnDeviceOrientation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root">
|
||||||
|
TODO
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ensureSignin } from '@/i';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,179 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root" :style="{ backgroundColor: bgColor }">
|
||||||
|
<div :class="$style.content" class="_spacer" :style="{ '--MI_SPACER-w': '512px' }">
|
||||||
|
<div :class="$style.qrOuter">
|
||||||
|
<div v-flip-on-device-orientation ref="qrCodeEl" :class="$style.qrInner"></div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.user">
|
||||||
|
<MkAvatar v-flip-on-device-orientation :class="$style.avatar" :user="$i" :indicator="false"/>
|
||||||
|
<div :class="$style.names">
|
||||||
|
<div v-flip-on-device-orientation :class="$style.username"><MkCondensedLine :minScale="2 / 3">@{{ $i.username }}@{{ host }}</MkCondensedLine></div>
|
||||||
|
<div v-flip-on-device-orientation :class="$style.name"><MkCondensedLine :minScale="2 / 3">{{ userName($i) }}</MkCondensedLine></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img v-flip-on-device-orientation :class="$style.logo" :src="misskeysvg" alt="Misskey Logo"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import QRCodeStyling from 'qr-code-styling';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { host } from '@@/js/config.js';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { ensureSignin } from '@/i';
|
||||||
|
import { userPage, userName } from '@/filters/user';
|
||||||
|
import misskeysvg from '/client-assets/misskey.svg';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const qrCodeEl = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const avatarColor = computed(() => tinycolor(
|
||||||
|
$i.avatarBlurhash ?
|
||||||
|
extractAvgColorFromBlurhash($i.avatarBlurhash)
|
||||||
|
: instance.themeColor
|
||||||
|
?? '#86b300',
|
||||||
|
));
|
||||||
|
const avatarHsl = computed(() => avatarColor.value.toHsl());
|
||||||
|
const bgColor = tinycolor(`hsl(${avatarHsl.value.h}, 60, 46)`).toRgbString();
|
||||||
|
|
||||||
|
watch([qrCodeEl, avatarHsl], () => {
|
||||||
|
const qrCodeInstance = new QRCodeStyling({
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
margin: 40,
|
||||||
|
type: 'canvas',
|
||||||
|
data: userPage($i, undefined, true),
|
||||||
|
image: instance.iconUrl ?? '/favicon.ico',
|
||||||
|
qrOptions: {
|
||||||
|
typeNumber: 0,
|
||||||
|
mode: 'Byte',
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
hideBackgroundDots: true,
|
||||||
|
imageSize: 0.3,
|
||||||
|
margin: 8,
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
dotsOptions: {
|
||||||
|
color: tinycolor(`hsl(${avatarHsl.value.h}, 100, 18)`).toRgbString(),
|
||||||
|
gradient: {
|
||||||
|
type: 'linear',
|
||||||
|
rotation: 1, // radian
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: tinycolor(`hsl(${avatarHsl.value.h}, 100, 6)`).toRgbString() },
|
||||||
|
{ offset: 0.5, color: tinycolor(`hsl(${avatarHsl.value.h}, 100, 18)`).toRgbString() },
|
||||||
|
{ offset: 1, color: tinycolor(`hsl(${avatarHsl.value.h}, 100, 25)`).toRgbString() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'classy-rounded',
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: tinycolor(`hsl(${avatarHsl.value.h}, 100, 97)`).toRgbString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qrCodeEl.value != null) {
|
||||||
|
qrCodeInstance.append(qrCodeEl.value);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
margin-top: calc( -1 * var(--MI-stickyTop) );
|
||||||
|
padding-top: var(--MI-stickyTop);
|
||||||
|
height: calc( 100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom) );
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrOuter {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrInner {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 40dvh;
|
||||||
|
margin: 5dvh auto 2dvh;
|
||||||
|
|
||||||
|
> svg,
|
||||||
|
> canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatarSize: 58px;
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
margin: 2dvh auto;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: clip;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: $avatarSize;
|
||||||
|
height: $avatarSize;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names {
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
margin-top: -4px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: inline-block;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: calc(100% - 16px);
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 16px;
|
||||||
|
mask-image: linear-gradient(90deg,#000,#000 calc(100% - 16px),#0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: calc(100% - 16px);
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 16px;
|
||||||
|
mask-image: linear-gradient(90deg,#000,#000 calc(100% - 16px),#0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 30%;
|
||||||
|
margin: 2dvh auto 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageWithHeader v-model:tab="tab" :tabs="headerTabs" :swipable="true" :style="{ overflow: tab === 'show' ? 'clip' : undefined }">
|
||||||
|
<MkQrShow v-if="tab === 'show'"/>
|
||||||
|
<MkQrRead v-else-if="tab === 'read'"/>
|
||||||
|
<MkError v-else-if="error" :error="error"/>
|
||||||
|
<MkLoading v-else/>
|
||||||
|
</PageWithHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||||
|
import { definePage } from '@/page.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { ensureSignin } from '@/i';
|
||||||
|
|
||||||
|
// router definitionでloginRequiredが設定されているためエラーハンドリングしない
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const tab = ref<'read' | 'show'>('show');
|
||||||
|
const error = ref<any>(null);
|
||||||
|
|
||||||
|
const MkQrShow = defineAsyncComponent(() => import('./qr.show.vue'));
|
||||||
|
const MkQrRead = defineAsyncComponent(() => import('./qr.read.vue'));
|
||||||
|
|
||||||
|
const headerTabs = [{
|
||||||
|
key: 'show',
|
||||||
|
title: i18n.ts._qr.showTabTitle,
|
||||||
|
icon: 'ti ti-qrcode',
|
||||||
|
}, {
|
||||||
|
key: 'read',
|
||||||
|
title: i18n.ts._qr.readTabTitle,
|
||||||
|
icon: 'ti ti-scan',
|
||||||
|
}];
|
||||||
|
|
||||||
|
definePage(() => ({
|
||||||
|
title: i18n.ts.qr,
|
||||||
|
icon: 'ti ti-qrcode',
|
||||||
|
}));
|
||||||
|
</script>
|
|
@ -590,6 +590,10 @@ export const ROUTE_DEF = [{
|
||||||
path: '/reversi/g/:gameId',
|
path: '/reversi/g/:gameId',
|
||||||
component: page(() => import('@/pages/reversi/game.vue')),
|
component: page(() => import('@/pages/reversi/game.vue')),
|
||||||
loginRequired: false,
|
loginRequired: false,
|
||||||
|
}, {
|
||||||
|
path: '/qr',
|
||||||
|
component: page(() => import('@/pages/qr.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
path: '/debug',
|
path: '/debug',
|
||||||
component: page(() => import('@/pages/debug.vue')),
|
component: page(() => import('@/pages/debug.vue')),
|
||||||
|
|
|
@ -844,6 +844,9 @@ importers:
|
||||||
punycode.js:
|
punycode.js:
|
||||||
specifier: 2.3.1
|
specifier: 2.3.1
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
|
qr-code-styling:
|
||||||
|
specifier: 1.9.2
|
||||||
|
version: 1.9.2
|
||||||
rollup:
|
rollup:
|
||||||
specifier: 4.48.0
|
specifier: 4.48.0
|
||||||
version: 4.48.0
|
version: 4.48.0
|
||||||
|
@ -9508,6 +9511,13 @@ packages:
|
||||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
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:
|
qrcode@1.5.4:
|
||||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
@ -21080,6 +21090,12 @@ snapshots:
|
||||||
|
|
||||||
pvutils@1.1.3: {}
|
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:
|
qrcode@1.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
dijkstrajs: 1.0.2
|
dijkstrajs: 1.0.2
|
||||||
|
|
Loading…
Reference in New Issue