perf: MkImgWithBlurhashとMkMediaImageを最適化 (#10782)
* #10781 * fix tsconfig * fetch image?? * Revert "fetch image??" This reverts commit0925c28d5a
. * wip * Revert "wip" This reverts commitbe97c6cb88
. * loading="eager" * loading="eager" 2 * error * wip * wip * wip * wip * clean up * fix * 生成するworkerを1つにする? * clean up * use buraha * wip * smaller width, height * update buraha * clean up * fix * Update MkMediaImage.vue * Update MkImgWithBlurhash.vue * Revert "fix(frontend): センシティブ設定された画像を開くとき一瞬レイアウトが崩れる問題を修正" This reverts commit41e9aa6f9b
. * Update MkMediaList.vue * Update MkMediaList.vue * Update MkMediaList.vue * Update CHANGELOG.md * wait for decode * fix * ? * (test) remove container-type: inline-size; * Revert "(test) remove container-type: inline-size;" This reverts commit9448e64228
. * container-name * Revert "container-name" This reverts commit94385d3221
. * width: 100%; * improve performance * refactor * wip * WIP * wip * Revert "wip" This reverts commit36e3b75cab
. * Revert "WIP" This reverts commit05b729ef91
. * Revert "wip" This reverts commit0801e79361
. * #10860 * wip * no worker * Revert "no worker" This reverts commita9c49e4fb4
. * ✌️ * workerNumber固定は不要 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
3804c6e7ad
commit
59255e11b8
|
@ -103,6 +103,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
||||||
* 画像が全て隠れた状態で表示されるようになります
|
* 画像が全て隠れた状態で表示されるようになります
|
||||||
- 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように
|
- 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように
|
||||||
- モデレーターはノートに添付された画像上から直接NSFW設定できるように
|
- モデレーターはノートに添付された画像上から直接NSFW設定できるように
|
||||||
|
- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
|
||||||
- プロフィール設定「追加情報」の項目の削除と並び替えができるように
|
- プロフィール設定「追加情報」の項目の削除と並び替えができるように
|
||||||
- 新しい実績を追加
|
- 新しい実績を追加
|
||||||
- AiScriptを0.13.2に更新
|
- AiScriptを0.13.2に更新
|
||||||
|
|
|
@ -25,9 +25,9 @@
|
||||||
"@vue-macros/reactivity-transform": "0.3.7",
|
"@vue-macros/reactivity-transform": "0.3.7",
|
||||||
"@vue/compiler-sfc": "3.3.2",
|
"@vue/compiler-sfc": "3.3.2",
|
||||||
"autosize": "6.0.1",
|
"autosize": "6.0.1",
|
||||||
"blurhash": "2.0.5",
|
|
||||||
"broadcast-channel": "4.20.2",
|
"broadcast-channel": "4.20.2",
|
||||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
|
"buraha": "github:misskey-dev/buraha",
|
||||||
"canvas-confetti": "1.6.0",
|
"canvas-confetti": "1.6.0",
|
||||||
"chart.js": "4.3.0",
|
"chart.js": "4.3.0",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
|
|
|
@ -5,12 +5,9 @@
|
||||||
<ImgWithBlurhash
|
<ImgWithBlurhash
|
||||||
class="img layered"
|
class="img layered"
|
||||||
:transition="safe ? null : {
|
:transition="safe ? null : {
|
||||||
enterActiveClass: $style.transition_toggle_enterActive,
|
duration: 500,
|
||||||
leaveActiveClass: $style.transition_toggle_leaveActive,
|
leaveActiveClass: $style.transition_toggle_leaveActive,
|
||||||
enterFromClass: $style.transition_toggle_enterFrom,
|
|
||||||
leaveToClass: $style.transition_toggle_leaveTo,
|
leaveToClass: $style.transition_toggle_leaveTo,
|
||||||
enterToClass: $style.transition_toggle_enterTo,
|
|
||||||
leaveFromClass: $style.transition_toggle_leaveFrom,
|
|
||||||
}"
|
}"
|
||||||
:src="post.files[0].thumbnailUrl"
|
:src="post.files[0].thumbnailUrl"
|
||||||
:hash="post.files[0].blurhash"
|
:hash="post.files[0].blurhash"
|
||||||
|
@ -53,24 +50,16 @@ function leaveHover(): void {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.transition_toggle_enterActive,
|
|
||||||
.transition_toggle_leaveActive {
|
.transition_toggle_leaveActive {
|
||||||
transition: opacity 0.5s;
|
transition: opacity .5s;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition_toggle_enterFrom,
|
|
||||||
.transition_toggle_leaveTo {
|
.transition_toggle_leaveTo {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition_toggle_enterTo,
|
|
||||||
.transition_toggle_leaveFrom {
|
|
||||||
transition: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,30 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||||
<img v-if="!loaded && src && !forceBlurhash" :class="$style.loader" :src="src" @load="onLoad"/>
|
<TransitionGroup
|
||||||
<Transition
|
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
|
||||||
mode="in-out"
|
:enter-active-class="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
|
||||||
:enter-active-class="defaultStore.state.animation && (props.transition?.enterActiveClass ?? $style['transition_toggle_enterActive']) || undefined"
|
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
|
||||||
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_toggle_leaveActive']) || undefined"
|
|
||||||
:enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
:enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||||
:leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
:leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||||
:enter-to-class="defaultStore.state.animation && (props.transition?.enterToClass ?? $style['transition_toggle_enterTo']) || undefined"
|
:enter-to-class="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||||
:leave-from-class="defaultStore.state.animation && (props.transition?.leaveFromClass ?? $style['transition_toggle_leaveFrom']) || undefined"
|
:leave-from-class="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||||
>
|
>
|
||||||
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? undefined"/>
|
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
||||||
<img v-else :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined"/>
|
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
||||||
</Transition>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { onMounted, shallowRef, useCssModule, watch } from 'vue';
|
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||||
import { decode } from 'blurhash';
|
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||||
import { defaultStore } from '@/store';
|
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
|
||||||
|
import { $ref } from 'vue/macros';
|
||||||
|
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||||
|
|
||||||
|
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
|
||||||
|
const testWorker = new TestWebGL2();
|
||||||
|
testWorker.addEventListener('message', event => {
|
||||||
|
if (event.data.result) {
|
||||||
|
const workers = new WorkerMultiDispatch(
|
||||||
|
() => new DrawBlurhash(),
|
||||||
|
Math.min(navigator.hardwareConcurrency - 1, 4),
|
||||||
|
);
|
||||||
|
resolve(workers);
|
||||||
|
if (_DEV_) console.log('WebGL2 in worker is supported!');
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
if (_DEV_) console.log('WebGL2 in worker is not supported...');
|
||||||
|
}
|
||||||
|
testWorker.terminate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { render } from 'buraha';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
transition?: {
|
transition?: {
|
||||||
|
duration?: number | { enter: number; leave: number; };
|
||||||
enterActiveClass?: string;
|
enterActiveClass?: string;
|
||||||
leaveActiveClass?: string;
|
leaveActiveClass?: string;
|
||||||
enterFromClass?: string;
|
enterFromClass?: string;
|
||||||
|
@ -51,67 +77,141 @@ const props = withDefaults(defineProps<{
|
||||||
forceBlurhash: false,
|
forceBlurhash: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const viewId = uuid();
|
||||||
const canvas = shallowRef<HTMLCanvasElement>();
|
const canvas = shallowRef<HTMLCanvasElement>();
|
||||||
|
const root = shallowRef<HTMLDivElement>();
|
||||||
|
const img = shallowRef<HTMLImageElement>();
|
||||||
let loaded = $ref(false);
|
let loaded = $ref(false);
|
||||||
let width = $ref(props.width);
|
let canvasWidth = $ref(64);
|
||||||
let height = $ref(props.height);
|
let canvasHeight = $ref(64);
|
||||||
|
let imgWidth = $ref(props.width);
|
||||||
|
let imgHeight = $ref(props.height);
|
||||||
|
let bitmapTmp = $ref<CanvasImageSource | undefined>();
|
||||||
|
const hide = computed(() => !loaded || props.forceBlurhash);
|
||||||
|
|
||||||
function onLoad() {
|
function waitForDecode() {
|
||||||
|
if (props.src != null && props.src !== '') {
|
||||||
|
nextTick()
|
||||||
|
.then(() => img.value?.decode())
|
||||||
|
.then(() => {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
}, error => {
|
||||||
|
console.error('Error occured during decoding image', img.value, error);
|
||||||
|
throw Error(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loaded = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([() => props.width, () => props.height], () => {
|
watch([() => props.width, () => props.height, root], () => {
|
||||||
const ratio = props.width / props.height;
|
const ratio = props.width / props.height;
|
||||||
if (ratio > 1) {
|
if (ratio > 1) {
|
||||||
width = Math.round(64 * ratio);
|
canvasWidth = Math.round(64 * ratio);
|
||||||
height = 64;
|
canvasHeight = 64;
|
||||||
} else {
|
} else {
|
||||||
width = 64;
|
canvasWidth = 64;
|
||||||
height = Math.round(64 / ratio);
|
canvasHeight = Math.round(64 / ratio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientWidth = root.value?.clientWidth ?? 300;
|
||||||
|
imgWidth = clientWidth;
|
||||||
|
imgHeight = Math.round(clientWidth / ratio);
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function draw() {
|
function drawImage(bitmap: CanvasImageSource) {
|
||||||
if (props.hash == null || !canvas.value) return;
|
// canvasがない(mountedされていない)場合はTmpに保存しておく
|
||||||
const pixels = decode(props.hash, width, height);
|
if (!canvas.value) {
|
||||||
|
bitmapTmp = bitmap;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// canvasがあれば描画する
|
||||||
|
bitmapTmp = undefined;
|
||||||
const ctx = canvas.value.getContext('2d');
|
const ctx = canvas.value.getContext('2d');
|
||||||
const imageData = ctx!.createImageData(width, height);
|
if (!ctx) return;
|
||||||
imageData.data.set(pixels);
|
ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
|
||||||
ctx!.putImageData(imageData, 0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([() => props.hash, canvas], () => {
|
async function draw() {
|
||||||
|
if (!canvas.value || props.hash == null) return;
|
||||||
|
|
||||||
|
const ctx = canvas.value.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// avgColorでお茶をにごす
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
|
||||||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
const workers = await workerPromise;
|
||||||
|
if (workers) {
|
||||||
|
workers.postMessage(
|
||||||
|
{
|
||||||
|
id: viewId,
|
||||||
|
hash: props.hash,
|
||||||
|
width: canvasWidth,
|
||||||
|
height: canvasHeight,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const work = document.createElement('canvas');
|
||||||
|
work.width = canvasWidth;
|
||||||
|
work.height = canvasHeight;
|
||||||
|
render(props.hash, work);
|
||||||
|
ctx.drawImage(work, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error occured during drawing blurhash', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function workerOnMessage(event: MessageEvent) {
|
||||||
|
if (event.data.id !== viewId) return;
|
||||||
|
drawImage(event.data.bitmap as ImageBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
workerPromise.then(worker => {
|
||||||
|
if (worker) {
|
||||||
|
worker.addListener(workerOnMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.src, () => {
|
||||||
|
waitForDecode();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.hash, () => {
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
draw();
|
// drawImageがmountedより先に呼ばれている場合はここで描画する
|
||||||
|
if (bitmapTmp) {
|
||||||
|
drawImage(bitmapTmp);
|
||||||
|
}
|
||||||
|
waitForDecode();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
workerPromise.then(worker => {
|
||||||
|
worker?.removeListener(workerOnMessage);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.transition_toggle_enterActive,
|
.transition_leaveActive {
|
||||||
.transition_toggle_leaveActive {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition_toggle_enterTo,
|
|
||||||
.transition_toggle_leaveFrom {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,6 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode"/>
|
<a
|
||||||
|
:class="$style.imageContainer"
|
||||||
|
:href="image.url"
|
||||||
|
:title="image.name"
|
||||||
|
>
|
||||||
|
<ImgWithBlurhash
|
||||||
|
:hash="image.blurhash"
|
||||||
|
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||||
|
:force-blurhash="hide"
|
||||||
|
:cover="hide"
|
||||||
|
:alt="image.comment || image.name"
|
||||||
|
:title="image.comment || image.name"
|
||||||
|
:width="image.properties.width"
|
||||||
|
:height="image.properties.height"
|
||||||
|
:style="hide ? 'filter: brightness(0.5);' : null"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<template v-if="hide">
|
||||||
<div :class="$style.hiddenText">
|
<div :class="$style.hiddenText">
|
||||||
<div :class="$style.hiddenTextWrapper">
|
<div :class="$style.hiddenTextWrapper">
|
||||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||||
|
@ -8,22 +25,16 @@
|
||||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
|
<template v-else>
|
||||||
<a
|
|
||||||
:class="$style.imageContainer"
|
|
||||||
:href="image.url"
|
|
||||||
:title="image.name"
|
|
||||||
>
|
|
||||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
|
||||||
</a>
|
|
||||||
<div :class="$style.indicators">
|
<div :class="$style.indicators">
|
||||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click.stop.prevent="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
|
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -53,6 +64,12 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||||
: props.image.thumbnailUrl,
|
: props.image.thumbnailUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onclick() {
|
||||||
|
if (hide) {
|
||||||
|
hide = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||||
watch(() => props.image, () => {
|
watch(() => props.image, () => {
|
||||||
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:class="[
|
:class="[
|
||||||
$style.medias,
|
$style.medias,
|
||||||
count <= 4 ? $style['n' + count] : $style.nMany,
|
count <= 4 ? $style['n' + count] : $style.nMany,
|
||||||
|
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`]
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, useCssModule, watch } from 'vue';
|
import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
import PhotoSwipe from 'photoswipe';
|
import PhotoSwipe from 'photoswipe';
|
||||||
|
@ -38,11 +39,42 @@ const props = defineProps<{
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const gallery = ref<HTMLDivElement>();
|
const gallery = shallowRef<HTMLDivElement>();
|
||||||
const pswpZIndex = os.claimZIndex('middle');
|
const pswpZIndex = os.claimZIndex('middle');
|
||||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||||
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||||
|
|
||||||
|
function calcAspectRatio() {
|
||||||
|
if (!gallery.value) return;
|
||||||
|
|
||||||
|
let img = props.mediaList[0];
|
||||||
|
|
||||||
|
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||||
|
gallery.value.style.aspectRatio = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// アスペクト比上限設定では、横長の場合は高さを縮小させる
|
||||||
|
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
|
||||||
|
|
||||||
|
switch (defaultStore.state.mediaListWithOneImageAppearance) {
|
||||||
|
case '16_9':
|
||||||
|
gallery.value.style.aspectRatio = ratioMax(16 / 9);
|
||||||
|
break;
|
||||||
|
case '1_1':
|
||||||
|
gallery.value.style.aspectRatio = ratioMax(1);
|
||||||
|
break;
|
||||||
|
case '2_3':
|
||||||
|
gallery.value.style.aspectRatio = ratioMax(2 / 3);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
gallery.value.style.aspectRatio = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const lightbox = new PhotoSwipeLightbox({
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
dataSource: props.mediaList
|
dataSource: props.mediaList
|
||||||
|
@ -162,12 +194,37 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
|
|
||||||
// for webkit
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&.n1 {
|
&.n1 {
|
||||||
aspect-ratio: 16/9;
|
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
|
|
||||||
|
// default (expand)
|
||||||
|
min-height: 64px;
|
||||||
|
max-height: clamp(
|
||||||
|
64px,
|
||||||
|
50cqh,
|
||||||
|
min(360px, 50vh)
|
||||||
|
);
|
||||||
|
|
||||||
|
&.n116_9 {
|
||||||
|
min-height: none;
|
||||||
|
max-height: none;
|
||||||
|
aspect-ratio: 16 / 9; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
&.n11_1{
|
||||||
|
min-height: none;
|
||||||
|
max-height: none;
|
||||||
|
aspect-ratio: 1 / 1; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
&.n12_3 {
|
||||||
|
min-height: none;
|
||||||
|
max-height: none;
|
||||||
|
aspect-ratio: 2 / 3; // fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.n2 {
|
&.n2 {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||||
<img :class="$style.inner" :src="url" decoding="async"/>
|
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
|
||||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||||
<div v-if="user.isCat" :class="[$style.ears]">
|
<div v-if="user.isCat" :class="[$style.ears]">
|
||||||
<div :class="$style.earLeft">
|
<div :class="$style.earLeft">
|
||||||
|
@ -30,6 +30,7 @@ import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-bl
|
||||||
import { acct, userPage } from '@/filters/user';
|
import { acct, userPage } from '@/filters/user';
|
||||||
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
|
||||||
|
|
||||||
const animation = $ref(defaultStore.state.animation);
|
const animation = $ref(defaultStore.state.animation);
|
||||||
const squareAvatars = $ref(defaultStore.state.squareAvatars);
|
const squareAvatars = $ref(defaultStore.state.squareAvatars);
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
|
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
|
||||||
<option value="force">{{ i18n.ts._nsfw.force }}</option>
|
<option value="force">{{ i18n.ts._nsfw.force }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<!--
|
|
||||||
<MkRadios v-model="mediaListWithOneImageAppearance">
|
<MkRadios v-model="mediaListWithOneImageAppearance">
|
||||||
<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
|
<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
|
||||||
<option value="expand">{{ i18n.ts.default }}</option>
|
<option value="expand">{{ i18n.ts.default }}</option>
|
||||||
|
@ -64,7 +64,6 @@
|
||||||
<option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
|
<option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
|
||||||
<option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
|
<option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
|
||||||
|
return prev + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||||
|
private symbol = Symbol('WorkerMultiDispatch');
|
||||||
|
private workers: Worker[] = [];
|
||||||
|
private terminated = false;
|
||||||
|
private prevWorkerNumber = 0;
|
||||||
|
private getUseWorkerNumber = defaultUseWorkerNumber;
|
||||||
|
private finalizationRegistry: FinalizationRegistry<symbol>;
|
||||||
|
|
||||||
|
constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
|
||||||
|
this.getUseWorkerNumber = getUseWorkerNumber;
|
||||||
|
for (let i = 0; i < concurrency; i++) {
|
||||||
|
this.workers.push(workerConstructor());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finalizationRegistry = new FinalizationRegistry(() => {
|
||||||
|
this.terminate();
|
||||||
|
});
|
||||||
|
this.finalizationRegistry.register(this, this.symbol);
|
||||||
|
|
||||||
|
if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
|
||||||
|
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
||||||
|
workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
|
||||||
|
if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
|
||||||
|
this.prevWorkerNumber = workerNumber;
|
||||||
|
|
||||||
|
// 不毛だがunionをoverloadに突っ込めない
|
||||||
|
// https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/14107
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
this.workers[workerNumber].postMessage(message, options);
|
||||||
|
} else {
|
||||||
|
this.workers[workerNumber].postMessage(message, options);
|
||||||
|
}
|
||||||
|
return workerNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||||
|
this.workers.forEach(worker => {
|
||||||
|
worker.addEventListener('message', callback, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||||
|
this.workers.forEach(worker => {
|
||||||
|
worker.removeEventListener('message', callback, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public terminate() {
|
||||||
|
this.terminated = true;
|
||||||
|
if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
|
||||||
|
this.workers.forEach(worker => {
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
this.workers = [];
|
||||||
|
this.finalizationRegistry.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isTerminated() {
|
||||||
|
return this.terminated;
|
||||||
|
}
|
||||||
|
public getWorkers() {
|
||||||
|
return this.workers;
|
||||||
|
}
|
||||||
|
public getSymbol() {
|
||||||
|
return this.symbol;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { render } from 'buraha';
|
||||||
|
|
||||||
|
onmessage = (event) => {
|
||||||
|
// console.log(event.data);
|
||||||
|
if (!('id' in event.data && typeof event.data.id === 'string')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!('hash' in event.data && typeof event.data.hash === 'string')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const work = new OffscreenCanvas(event.data.width ?? 64, event.data.height ?? 64);
|
||||||
|
render(event.data.hash, work);
|
||||||
|
const bitmap = work.transferToImageBitmap();
|
||||||
|
postMessage({ id: event.data.id, bitmap });
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
const canvas = new OffscreenCanvas(1, 1);
|
||||||
|
const gl = canvas.getContext('webgl2');
|
||||||
|
if (gl) {
|
||||||
|
postMessage({ result: true });
|
||||||
|
} else {
|
||||||
|
postMessage({ result: false });
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["esnext", "webworker"],
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,6 +139,10 @@ export function getConfig(): UserConfig {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
worker: {
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
deps: {
|
deps: {
|
||||||
|
|
|
@ -657,15 +657,15 @@ importers:
|
||||||
autosize:
|
autosize:
|
||||||
specifier: 6.0.1
|
specifier: 6.0.1
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
blurhash:
|
|
||||||
specifier: 2.0.5
|
|
||||||
version: 2.0.5
|
|
||||||
broadcast-channel:
|
broadcast-channel:
|
||||||
specifier: 4.20.2
|
specifier: 4.20.2
|
||||||
version: 4.20.2
|
version: 4.20.2
|
||||||
browser-image-resizer:
|
browser-image-resizer:
|
||||||
specifier: github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3
|
specifier: github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3
|
||||||
version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a
|
version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a
|
||||||
|
buraha:
|
||||||
|
specifier: github:misskey-dev/buraha
|
||||||
|
version: github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c
|
||||||
canvas-confetti:
|
canvas-confetti:
|
||||||
specifier: 1.6.0
|
specifier: 1.6.0
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
|
@ -20410,6 +20410,12 @@ packages:
|
||||||
version: 2.2.1-misskey.3
|
version: 2.2.1-misskey.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c:
|
||||||
|
resolution: {tarball: https://codeload.github.com/misskey-dev/buraha/tar.gz/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c}
|
||||||
|
name: buraha
|
||||||
|
version: 0.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01:
|
github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01:
|
||||||
resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/02d9dc189fa7df0c4bea09330be26741772dac01}
|
resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/02d9dc189fa7df0c4bea09330be26741772dac01}
|
||||||
name: sharp-read-bmp
|
name: sharp-read-bmp
|
||||||
|
|
Loading…
Reference in New Issue