This commit is contained in:
tamaina 2025-08-25 14:39:45 +09:00
parent f1d27dcd6c
commit bb09f08f84
4 changed files with 113 additions and 65 deletions

View File

@ -1,45 +0,0 @@
import type { Directive } from 'vue';
let initialized = false;
let styleEl: HTMLStyleElement | null = null;
const canUseDeviceOrientation = !!window.DeviceOrientationEvent;
const className = '_flipOnDeviceOrientation';
const variableName = `--MI-flip_on_device_orientation_transform`;
function handleOrientationChange(event: DeviceOrientationEvent) {
const isUpsideDown = event.beta ? event.beta < 5 : false;
const transform = isUpsideDown ? 'scale(-1, -1)' : '';
window.document.documentElement.style.setProperty(variableName, transform);
}
function registerListener() {
if (!canUseDeviceOrientation) return;
if (!initialized) {
window.addEventListener('deviceorientation', 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) {
window.removeEventListener('deviceorientation', handleOrientationChange);
if (styleEl) {
window.document.head.removeChild(styleEl);
styleEl = null;
}
initialized = false;
}
}
export default {
mounted(el) {
registerListener();
el.classList.add(className);
},
unmounted(el) {
el.classList.remove(className);
registerListener();
},
} as Directive;

View File

@ -16,7 +16,6 @@ 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)) {
@ -37,5 +36,4 @@ export const directives = {
'panel': panel,
'adaptive-border': adaptiveBorder,
'adaptive-bg': adaptiveBg,
'flip-on-device-orientation': flipOnDeviceOrientation,
};

View File

@ -4,7 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" :style="{ backgroundColor: bgColor }">
<div ref="rootEl" :class="$style.root" :style="{
backgroundColor: bgColor,
minHeight: container ?
`calc( ${scrollHeight}px - var(--MI-stickyTop) - var(--MI-stickyBottom) )`
: `calc( 100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom) )`,
}">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5" />
<div :class="$style.fg">
<div
@ -12,20 +17,26 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="share"
:style="{
'--MI_SPACER-w': '512px',
'--MI_SPACER-max': '16px',
'cursor': canShare ? 'pointer' : 'default',
}"
>
<div :class="$style.qrOuter">
<div v-flip-on-device-orientation ref="qrCodeEl" :class="$style.qrInner"></div>
<div
:class="$style.qrOuter"
:style="{
maxHeight: container ? `max(256px, ${scrollHeight * 0.5}px)` : `max(256px, 50dvh)`,
}"
>
<div v-flip ref="qrCodeEl" :class="$style.qrInner"></div>
</div>
<div :class="$style.user">
<MkAvatar v-flip-on-device-orientation :class="$style.avatar" :user="$i" :indicator="false"/>
<MkAvatar v-flip :class="$style.avatar" :user="$i" :indicator="false"/>
<div :class="$style.names">
<div v-flip-on-device-orientation :class="$style.username"><MkCondensedLine :minScale="2 / 3">{{ acct }}</MkCondensedLine></div>
<div v-flip-on-device-orientation :class="$style.name"><MkCondensedLine :minScale="2 / 3">{{ userName($i) }}</MkCondensedLine></div>
<div v-flip :class="$style.username"><MkCondensedLine :minScale="2 / 3">{{ acct }}</MkCondensedLine></div>
<div v-flip :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"/>
<img v-flip :class="$style.logo" :src="misskeysvg" alt="Misskey Logo"/>
</div>
</div>
</div>
@ -35,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
import tinycolor from 'tinycolor2';
import QRCodeStyling from 'qr-code-styling';
import { computed, ref, watch, onMounted } from 'vue';
import { computed, ref, watch, onMounted, type Directive, useCssModule, onUnmounted, type ComponentPublicInstance, nextTick } from 'vue';
import { host } from '@@/js/config.js';
import { instance } from '@/instance.js';
import { ensureSignin } from '@/i.js';
@ -44,9 +55,16 @@ import misskeysvg from '/client-assets/misskey.svg';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@@/js/scroll';
const $style = useCssModule();
const $i = ensureSignin();
const container = ref<HTMLElement | null>(null);
const rootEl = ref<HTMLDivElement | null>(null);
const scrollHeight = ref(window.innerHeight);
const acct = computed(() => `@${$i.username}@${host}`);
const url = computed(() => userPage($i, undefined, true));
@ -67,7 +85,7 @@ function share() {
if (!canShare.value) return;
return navigator.share({
title: i18n.tsx._qr.shareTitle({ name: userName($i), acct: acct.value }),
text: i18n.tsx._qr.shareText({ name: userName($i), acct: acct.value }),
text: i18n.ts._qr.shareText,
url: url.value,
});
}
@ -114,18 +132,80 @@ watch([qrCodeEl, avatarHsl, url], () => {
}
}, { immediate: true });
//#region scroll height
function checkScrollHeight() {
scrollHeight.value = container.value ? container.value.clientHeight : window.innerHeight;
}
let ro: ResizeObserver | undefined;
onMounted(() => {
container.value = getScrollContainer(rootEl.value);
checkScrollHeight();
if (container.value) {
ro = new ResizeObserver(checkScrollHeight);
ro.observe(container.value);
}
});
onUnmounted(() => {
ro?.disconnect();
});
//#endregion
//#region flip
const flipEls: Set<Element> = new Set();
const flip = ref(false);
function handleOrientationChange(event: DeviceOrientationEvent) {
const isUpsideDown = event.beta ? event.beta < 5 : false;
flip.value = isUpsideDown;
}
watch(flip, (newState) => {
flipEls.forEach(el => {
el.classList.toggle($style.fliped, newState);
});
});
onMounted(() => {
window.addEventListener('deviceorientation', handleOrientationChange);
});
onUnmounted(() => {
window.removeEventListener('deviceorientation', handleOrientationChange);
});
const vFlip = {
mounted(el: Element) {
flipEls.add(el);
el.classList.add($style.flip);
},
unmounted(el: Element) {
el.classList.remove($style.flip);
flipEls.delete(el);
}
} as Directive;
//#endregion
</script>
<style lang="scss" module>
$s1: 16px;
$s2: 24px;
$s3: 32px;
.root {
position: relative;
margin-top: calc( -1 * var(--MI-stickyTop) );
margin-bottom: calc( -1 * var(--MI-stickyBottom) );
padding-top: var(--MI-stickyTop);
height: calc( 100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom) );
padding-bottom: var(--MI-stickyBottom);
height: 100%;
}
.fg {
position: absolute;
position: sticky;
top: var(--MI-stickyTop);
width: 100%;
height: 100%;
@ -138,14 +218,23 @@ watch([qrCodeEl, avatarHsl, url], () => {
margin: 0 auto;
}
.flip {
transition: scale rotate 0.3s ease-in;
}
.fliped {
rotate: x 1;
scale: y -1;
}
.qrOuter {
display: flex;
width: 100%;
padding: $s3 0;
}
.qrInner {
width: 100%;
max-height: 40dvh;
margin: 58px auto 32px;
margin: 0;
> svg,
> canvas {
@ -159,7 +248,7 @@ $avatarSize: 58px;
.user {
display: flex;
margin: 32px auto;
margin: $s3 auto;
justify-content: center;
align-items: center;
overflow: visible;
@ -207,7 +296,7 @@ $avatarSize: 58px;
}
.logo {
width: 30%;
margin: 32px auto 0;
width: 25%;
margin: $s3 auto 0;
}
</style>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader v-model:tab="tab" :tabs="headerTabs" :swipable="true" :style="{ overflowY: tab === 'show' ? 'clip' : undefined }">
<PageWithHeader :class="$style.root" v-model:tab="tab" :tabs="headerTabs" :swipable="true">
<MkQrShow v-if="tab === 'show'"/>
<MkQrRead v-else-if="tab === 'read'"/>
<MkError v-else-if="error" :error="error"/>
@ -42,3 +42,9 @@ definePage(() => ({
icon: 'ti ti-qrcode',
}));
</script>
<style lang="scss" module>
.root {
height: 100%;
}
</style>