mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-03 06:04:22 +02:00
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue) * added to navbar * qr.show.vue * fix * added to navbar * fix size * 🎨 * 🎨 * fix div warn * fix * use * 0.25 * fix?? * fix lint * clean up * ??? * ? * fix * 🎨 * 🎨 * refactor * 🎨 * 🎨 * :ar:t * 🎨 * iphone flip * no lazy import * 🎨 * 🎨 * 🎨 * ユーザー全部flipでいいや * ✌️ * fix * fix * fix lint * 🎨 * fix type * fix: local user profile url cannot be resolved with ap/show * fix: local user url with hostname could not be resolved * chore: use common utility for checking self host * wip * 🎨 * 🎨 * fix imports * fix * fix * fix * 🎨 * fix... * set spacer-w * ✌️ * 全画面でQRを読むように * fix * 🎨 * modify navbar.ts * start/stop on vue activation * display raw content read from qr * 端末のQRをスキャンするボタンを追加 * chore * やっぱりmfmを先に表示する * 🎨 * fix 18n * QRの内容は/users/:userIdにする * add spdx * use cqh * `defineProps` is a compiler macro and no longer needs to be imported. * use MkUserName * 🎨 * 🎨 * refactor * clean up * refactor * 🎨 * Update qr.show.vue * Misskeyロゴにdrop-shadowを追加 * clean up: do not use empty css * fix os.select usage * Update qr.vue * Update qr.show.vue * Update qr.show.vue * Update get-user-menu.ts * ✌️ * Update show.ts * Update ja-JP.yml * watermark * Update CHANGELOG.md * Update qr.read.vue * Update qr.read.vue * wip * Update MkWatermarkEditorDialog.Layer.vue --------- Co-authored-by: anatawa12 <anatawa12@icloud.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
234
packages/frontend/src/pages/qr.show.vue
Normal file
234
packages/frontend/src/pages/qr.show.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="[$style.content]">
|
||||
<div
|
||||
ref="qrCodeEl" v-flip :style="{
|
||||
'cursor': canShare ? 'pointer' : 'default',
|
||||
}"
|
||||
:class="$style.qr" @click="share"
|
||||
></div>
|
||||
<div v-flip :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="$i" :indicator="false"/>
|
||||
<div>
|
||||
<div :class="$style.name"><MkCondensedLine :minScale="2 / 3"><MkUserName :user="$i" :nowrap="true"/></MkCondensedLine></div>
|
||||
<div><MkCondensedLine :minScale="2 / 3">{{ acct }}</MkCondensedLine></div>
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="deviceMotionPermissionNeeded" v-flip :class="$style.logo" :src="misskeysvg" alt="Misskey Logo" @click="requestDeviceMotion"/>
|
||||
<img v-else v-flip :class="$style.logo" :src="misskeysvg" alt="Misskey Logo"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import tinycolor from 'tinycolor2';
|
||||
import QRCodeStyling from 'qr-code-styling';
|
||||
import { computed, ref, shallowRef, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import { url, host } from '@@/js/config.js';
|
||||
import type { Directive } from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { userPage, userName } from '@/filters/user.js';
|
||||
import misskeysvg from '/client-assets/misskey.svg';
|
||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const acct = computed(() => `@${$i.username}@${host}`);
|
||||
const userProfileUrl = computed(() => userPage($i, undefined, true));
|
||||
const shareData = computed(() => ({
|
||||
title: i18n.tsx._qr.shareTitle({ name: userName($i), acct: acct.value }),
|
||||
text: i18n.ts._qr.shareText,
|
||||
url: userProfileUrl.value,
|
||||
}));
|
||||
const canShare = computed(() => navigator.canShare && navigator.canShare(shareData.value));
|
||||
|
||||
const qrCodeEl = useTemplateRef('qrCodeEl');
|
||||
|
||||
const qrColor = computed(() => tinycolor(instance.themeColor ?? '#86b300'));
|
||||
const qrHsl = computed(() => qrColor.value.toHsl());
|
||||
|
||||
function share() {
|
||||
if (!canShare.value) return;
|
||||
return navigator.share(shareData.value);
|
||||
}
|
||||
|
||||
const qrCodeInstance = new QRCodeStyling({
|
||||
width: 600,
|
||||
height: 600,
|
||||
margin: 42,
|
||||
type: 'canvas',
|
||||
data: `${url}/users/${$i.id}`,
|
||||
image: instance.iconUrl ? getStaticImageUrl(instance.iconUrl) : '/favicon.ico',
|
||||
qrOptions: {
|
||||
typeNumber: 0,
|
||||
mode: 'Byte',
|
||||
errorCorrectionLevel: 'H',
|
||||
},
|
||||
imageOptions: {
|
||||
hideBackgroundDots: true,
|
||||
imageSize: 0.3,
|
||||
margin: 16,
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
dotsOptions: {
|
||||
type: 'dots',
|
||||
color: tinycolor(`hsl(${qrHsl.value.h}, 100, 18)`).toRgbString(),
|
||||
},
|
||||
cornersDotOptions: {
|
||||
type: 'dot',
|
||||
},
|
||||
cornersSquareOptions: {
|
||||
type: 'extra-rounded',
|
||||
},
|
||||
backgroundOptions: {
|
||||
color: tinycolor(`hsl(${qrHsl.value.h}, 100, 97)`).toRgbString(),
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (qrCodeEl.value != null) {
|
||||
qrCodeInstance.append(qrCodeEl.value);
|
||||
}
|
||||
});
|
||||
|
||||
//#region flip
|
||||
const THRESHOLD = -3;
|
||||
// @ts-expect-error TS(2339)
|
||||
const deviceMotionPermissionNeeded = window.DeviceMotionEvent && typeof window.DeviceMotionEvent.requestPermission === 'function';
|
||||
const flipEls: Set<Element> = new Set();
|
||||
const flip = ref(false);
|
||||
|
||||
function handleOrientationChange(event: DeviceOrientationEvent) {
|
||||
const isUpsideDown = event.beta ? event.beta < THRESHOLD : false;
|
||||
flip.value = isUpsideDown;
|
||||
}
|
||||
|
||||
watch(flip, (newState) => {
|
||||
flipEls.forEach(el => {
|
||||
el.classList.toggle('_qrShowFlipFliped', newState);
|
||||
});
|
||||
});
|
||||
|
||||
function requestDeviceMotion() {
|
||||
if (!deviceMotionPermissionNeeded) return;
|
||||
// @ts-expect-error TS(2339)
|
||||
window.DeviceMotionEvent.requestPermission()
|
||||
.then((response: string) => {
|
||||
if (response === 'granted') {
|
||||
window.addEventListener('deviceorientation', handleOrientationChange);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('deviceorientation', handleOrientationChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('deviceorientation', handleOrientationChange);
|
||||
});
|
||||
|
||||
const vFlip = {
|
||||
mounted(el: Element) {
|
||||
flipEls.add(el);
|
||||
el.classList.add('_qrShowFlip');
|
||||
},
|
||||
unmounted(el: Element) {
|
||||
el.classList.remove('_qrShowFlip');
|
||||
flipEls.delete(el);
|
||||
},
|
||||
} as Directive;
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$s1: 14px;
|
||||
$s2: 21px;
|
||||
$s3: 28px;
|
||||
$avatarSize: 58px;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qr {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 230px;
|
||||
border-radius: 12px;
|
||||
overflow: clip;
|
||||
aspect-ratio: 1;
|
||||
|
||||
> svg,
|
||||
> canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: $s3 auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
overflow: visible;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: $avatarSize;
|
||||
height: $avatarSize;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
margin: $s3 auto 0;
|
||||
filter: drop-shadow(0 0 6px #0007);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/*
|
||||
* useCssModuleで$styleを読み込みたかったが、rollupでのunwindが壊れてしまうらしく失敗。
|
||||
* グローバルにクラスを定義することでお茶を濁す。
|
||||
*/
|
||||
._qrShowFlip {
|
||||
transition: rotate .3s linear, scale .3s .15s step-start;
|
||||
}
|
||||
|
||||
._qrShowFlipFliped {
|
||||
scale: -1 1;
|
||||
rotate: x 180deg;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user