wip (qr.show.vue)
This commit is contained in:
parent
47d83e8930
commit
32b21b1186
|
@ -12548,6 +12548,20 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"listDrafts": string;
|
||||
};
|
||||
/**
|
||||
* 二次元コード
|
||||
*/
|
||||
"qr": string;
|
||||
"_qr": {
|
||||
/**
|
||||
* 表示
|
||||
*/
|
||||
"showTabTitle": string;
|
||||
/**
|
||||
* 読み取る
|
||||
*/
|
||||
"readTabTitle": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -3359,3 +3359,8 @@ _drafts:
|
|||
restoreFromDraft: "下書きから復元"
|
||||
restore: "復元"
|
||||
listDrafts: "下書き一覧"
|
||||
|
||||
qr: 二次元コード
|
||||
_qr:
|
||||
showTabTitle: "表示"
|
||||
readTabTitle: "読み取る"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 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,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
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')),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue