mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-27 16:34:35 +02:00
wip
This commit is contained in:
@@ -29,36 +29,50 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@transitioncancel.self="enableTransition = false"
|
||||
>
|
||||
<img
|
||||
v-if="!originalImageLoaded || !thumbnailImageLoaded"
|
||||
:class="[$style.image, $style.thumbnail]"
|
||||
:src="image.thumbnailUrl ?? image.url"
|
||||
:width="image.width"
|
||||
:height="image.height"
|
||||
:style="{ width: `${imageRenderingSize.width}px`, height: `${imageRenderingSize.height}px` }"
|
||||
v-if="!originalContentLoaded || !thumbnailContentLoaded"
|
||||
:class="[$style.content, $style.thumbnail]"
|
||||
:src="content.thumbnailUrl ?? content.url"
|
||||
:width="content.width === 0 || content.width == null ? undefined : content.width"
|
||||
:height="content.height === 0 || content.height == null ? undefined : content.height"
|
||||
:style="{ width: contentRenderingSize == null ? '100%' : `${contentRenderingSize.width}px`, height: contentRenderingSize == null ? 'auto' : `${contentRenderingSize.height}px` }"
|
||||
draggable="false"
|
||||
@load="thumbnailImageLoaded = true"
|
||||
>
|
||||
<img
|
||||
v-if="activated"
|
||||
ref="imageEl"
|
||||
:class="[$style.image, $style.original]"
|
||||
:src="image.url"
|
||||
:width="image.width"
|
||||
:height="image.height"
|
||||
:style="{ width: `${imageRenderingSize.width}px`, height: `${imageRenderingSize.height}px` }"
|
||||
draggable="false"
|
||||
@load="originalImageLoaded = true"
|
||||
@load="thumbnailContentLoaded = true"
|
||||
>
|
||||
|
||||
<template v-if="activated">
|
||||
<img
|
||||
v-if="content.type === 'image'"
|
||||
:class="[$style.content, $style.original]"
|
||||
:src="content.url"
|
||||
:width="content.width === 0 || content.width == null ? undefined : content.width"
|
||||
:height="content.height === 0 || content.height == null ? undefined : content.height"
|
||||
:style="{ width: contentRenderingSize == null ? '100%' : `${contentRenderingSize.width}px`, height: contentRenderingSize == null ? 'auto' : `${contentRenderingSize.height}px` }"
|
||||
draggable="false"
|
||||
@load="originalContentLoaded = true"
|
||||
>
|
||||
<video
|
||||
v-else-if="content.type === 'video'"
|
||||
:class="[$style.content, $style.original]"
|
||||
:src="content.url"
|
||||
:width="content.width === 0 || content.width == null ? undefined : content.width"
|
||||
:height="content.height === 0 || content.height == null ? undefined : content.height"
|
||||
:style="{ width: contentRenderingSize == null ? '100%' : `${contentRenderingSize.width}px`, height: contentRenderingSize == null ? 'auto' : `${contentRenderingSize.height}px` }"
|
||||
draggable="false"
|
||||
controls
|
||||
autoplay
|
||||
@loadedmetadata="originalContentLoaded = true"
|
||||
></video>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activated && !originalImageLoaded" :class="$style.loading">
|
||||
<div v-if="activated && !originalContentLoaded" :class="$style.loading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
|
||||
<div :class="[$style.footer, { [$style.infoShowing]: infoShowing && !isZooming }]">
|
||||
<div :class="$style.footerText">
|
||||
{{ image.comment ?? image.filename }}
|
||||
{{ content.comment ?? content.filename }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,12 +89,13 @@ type Rect = Size & {
|
||||
top: number;
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
export type Content = {
|
||||
id: string;
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
thumbnailUrl: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
filename?: string | null;
|
||||
comment?: string | null;
|
||||
sourceElement?: HTMLElement | null;
|
||||
@@ -88,25 +103,25 @@ export type Image = {
|
||||
|
||||
export function calculateSourceTransform({
|
||||
fit,
|
||||
imageRenderingRect,
|
||||
contentRenderingRect,
|
||||
sourceRect,
|
||||
}: {
|
||||
fit: string;
|
||||
imageRenderingRect: Rect;
|
||||
contentRenderingRect: Rect;
|
||||
sourceRect: Rect;
|
||||
}): { x: number; y: number; scale: number } {
|
||||
const scale = fit === 'cover'
|
||||
? Math.max(sourceRect.width / imageRenderingRect.width, sourceRect.height / imageRenderingRect.height)
|
||||
: Math.min(sourceRect.width / imageRenderingRect.width, sourceRect.height / imageRenderingRect.height);
|
||||
? Math.max(sourceRect.width / contentRenderingRect.width, sourceRect.height / contentRenderingRect.height)
|
||||
: Math.min(sourceRect.width / contentRenderingRect.width, sourceRect.height / contentRenderingRect.height);
|
||||
|
||||
const sourceImageWidth = imageRenderingRect.width * scale;
|
||||
const sourceImageHeight = imageRenderingRect.height * scale;
|
||||
const sourceImageLeft = sourceRect.left + (sourceRect.width - sourceImageWidth) / 2;
|
||||
const sourceImageTop = sourceRect.top + (sourceRect.height - sourceImageHeight) / 2;
|
||||
const sourceContentWidth = contentRenderingRect.width * scale;
|
||||
const sourceContentHeight = contentRenderingRect.height * scale;
|
||||
const sourceContentLeft = sourceRect.left + (sourceRect.width - sourceContentWidth) / 2;
|
||||
const sourceContentTop = sourceRect.top + (sourceRect.height - sourceContentHeight) / 2;
|
||||
|
||||
return {
|
||||
x: sourceImageLeft - imageRenderingRect.left * scale,
|
||||
y: sourceImageTop - imageRenderingRect.top * scale,
|
||||
x: sourceContentLeft - contentRenderingRect.left * scale,
|
||||
y: sourceContentTop - contentRenderingRect.top * scale,
|
||||
scale,
|
||||
};
|
||||
}
|
||||
@@ -120,7 +135,7 @@ import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { isTouchUsing } from '@/utility/touch.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
image: Image;
|
||||
content: Content;
|
||||
activated: boolean;
|
||||
}>(), {
|
||||
});
|
||||
@@ -135,10 +150,9 @@ const emit = defineEmits<{
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const mainEl = useTemplateRef('mainEl');
|
||||
const imageEl = useTemplateRef('imageEl');
|
||||
|
||||
const originalImageLoaded = ref(false);
|
||||
const thumbnailImageLoaded = ref(false);
|
||||
const originalContentLoaded = ref(false);
|
||||
const thumbnailContentLoaded = ref(false);
|
||||
const enableTransition = ref(false);
|
||||
const infoShowing = ref(false);
|
||||
|
||||
@@ -154,42 +168,44 @@ const headerAreaSize = 0;
|
||||
const footerAreaSize = 30;
|
||||
|
||||
// maxからはみ出す場合は縮小、maxに満たない場合は拡大する(contain)
|
||||
function calcImageRenderingSize(image: Image) {
|
||||
function calcContentRenderingSize(content: Content) {
|
||||
if (content.width == null || content.height == null || content.width === 0 || content.height === 0) return null;
|
||||
|
||||
const maxWidth = window.innerWidth - padding * 2;
|
||||
const maxHeight = window.innerHeight - headerAreaSize - footerAreaSize - padding * 2;
|
||||
|
||||
const widthRatio = maxWidth / image.width;
|
||||
const heightRatio = maxHeight / image.height;
|
||||
const widthRatio = maxWidth / content.width;
|
||||
const heightRatio = maxHeight / content.height;
|
||||
const ratio = widthRatio < heightRatio ? widthRatio : heightRatio;
|
||||
|
||||
const width = image.width * ratio;
|
||||
const height = image.height * ratio;
|
||||
const width = content.width * ratio;
|
||||
const height = content.height * ratio;
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
const imageRenderingSize = calcImageRenderingSize(props.image);
|
||||
const imageRenderingRect = {
|
||||
left: (window.innerWidth - imageRenderingSize.width) / 2,
|
||||
top: headerAreaSize + (window.innerHeight - headerAreaSize - footerAreaSize - imageRenderingSize.height) / 2,
|
||||
width: imageRenderingSize.width,
|
||||
height: imageRenderingSize.height,
|
||||
};
|
||||
const contentRenderingSize = calcContentRenderingSize(props.content);
|
||||
const contentRenderingRect = contentRenderingSize != null ? {
|
||||
left: (window.innerWidth - contentRenderingSize.width) / 2,
|
||||
top: headerAreaSize + (window.innerHeight - headerAreaSize - footerAreaSize - contentRenderingSize.height) / 2,
|
||||
width: contentRenderingSize.width,
|
||||
height: contentRenderingSize.height,
|
||||
} : null;
|
||||
const transform = ref({ x: 0, y: 0, scale: 1 });
|
||||
|
||||
// 元のimg要素の位置・サイズ(とobject-fitの設定値)を取得して、そこからneutralの位置にアニメーションするためのscaleとtranslationを計算する
|
||||
function getScaleAndTranslationForSourceElement(): { x: number; y: number; scale: number } {
|
||||
const sourceElement = props.image.sourceElement;
|
||||
if (sourceElement == null) return { x: 0, y: 0, scale: 1 };
|
||||
const sourceElement = props.content.sourceElement;
|
||||
if (sourceElement == null || contentRenderingRect == null) return { x: 0, y: 0, scale: 1 };
|
||||
|
||||
return calculateSourceTransform({
|
||||
fit: window.getComputedStyle(sourceElement).objectFit,
|
||||
imageRenderingRect,
|
||||
contentRenderingRect,
|
||||
sourceRect: sourceElement.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
if (props.image.sourceElement != null && props.activated) {
|
||||
if (props.content.sourceElement != null && props.activated) {
|
||||
const sourceTransform = getScaleAndTranslationForSourceElement();
|
||||
transform.value.scale = sourceTransform.scale;
|
||||
transform.value.x = sourceTransform.x;
|
||||
@@ -496,10 +512,10 @@ watch(isZooming, () => {
|
||||
});
|
||||
//#endregion
|
||||
|
||||
watch(thumbnailImageLoaded, () => {
|
||||
watch(thumbnailContentLoaded, () => {
|
||||
if (rootEl.value == null) return;
|
||||
|
||||
const sourceElement = props.image.sourceElement;
|
||||
const sourceElement = props.content.sourceElement;
|
||||
if (sourceElement != null && props.activated) {
|
||||
enableTransition.value = true;
|
||||
rootEl.value.offsetHeight; // reflow
|
||||
@@ -541,7 +557,7 @@ function onCLick() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
.content {
|
||||
display: block;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
|
||||
@@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div
|
||||
ref="itemsEl"
|
||||
:class="[$style.items, { [$style.itemsTransition]: enableSlideTransition }]"
|
||||
:style="{ translate: `${imagesOffset}px 0` }"
|
||||
:style="{ translate: `${contentsOffset}px 0` }"
|
||||
@transitionend.self="onSlideTransitionFinished"
|
||||
@transitioncancel.self="onSlideTransitionFinished"
|
||||
>
|
||||
<div v-for="(image, i) in images" :key="image.url" ref="itemEl" :class="$style.item">
|
||||
<div v-for="(content, i) in contents" :key="content.url" ref="itemEl" :class="$style.item">
|
||||
<XItem
|
||||
:image="image"
|
||||
:content="content"
|
||||
:activated="activatedIndexes.has(i)"
|
||||
@close="onItemClose"
|
||||
@horizontalSwipe="onHorizontalSwipe"
|
||||
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
|
||||
<button v-if="!isTouchUsing && currentIndex > 0" class="_button" :class="[$style.prevButton]" @click="onPrev"><div :class="$style.buttonIcon"><i class="ti ti-arrow-left"></i></div></button>
|
||||
<button v-if="!isTouchUsing && currentIndex < images.length - 1" class="_button" :class="[$style.nextButton]" @click="onNext"><div :class="$style.buttonIcon"><i class="ti ti-arrow-right"></i></div></button>
|
||||
<button v-if="!isTouchUsing && currentIndex < contents.length - 1" class="_button" :class="[$style.nextButton]" @click="onNext"><div :class="$style.buttonIcon"><i class="ti ti-arrow-right"></i></div></button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import XItem from './MkImageGallery.item.vue';
|
||||
import type { Image } from './MkImageGallery.item.vue';
|
||||
import type { Content } from './MkImageGallery.item.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
@@ -56,7 +56,7 @@ import { isTouchUsing } from '@/utility/touch.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultIndex?: number;
|
||||
images: Image[];
|
||||
contents: Content[];
|
||||
}>(), {
|
||||
});
|
||||
|
||||
@@ -70,10 +70,10 @@ watch(currentIndex, (newIndex) => {
|
||||
activatedIndexes.value.add(newIndex);
|
||||
}, { immediate: true });
|
||||
watch(currentIndex, (newIndex) => {
|
||||
for (let i = 0; i < props.images.length; i++) {
|
||||
const image = props.images[i];
|
||||
if (image.sourceElement != null) {
|
||||
image.sourceElement.style.visibility = i === newIndex ? 'hidden' : '';
|
||||
for (let i = 0; i < props.contents.length; i++) {
|
||||
const content = props.contents[i];
|
||||
if (content.sourceElement != null) {
|
||||
content.sourceElement.style.visibility = i === newIndex ? 'hidden' : '';
|
||||
}
|
||||
}
|
||||
}, { immediate: false });
|
||||
@@ -84,17 +84,17 @@ const slideAnimDuration = 300;
|
||||
const zIndex = os.claimZIndex('high');
|
||||
const showing = ref(true);
|
||||
const screenWidth = ref(window.innerWidth);
|
||||
const imagesOffset = ref(currentIndex.value * -window.innerWidth);
|
||||
const contentsOffset = ref(currentIndex.value * -window.innerWidth);
|
||||
const enableSlideTransition = ref(false);
|
||||
let currentScrollLeft = imagesOffset.value;
|
||||
let currentScrollLeft = contentsOffset.value;
|
||||
|
||||
function onHorizontalSwipe(offset: number) {
|
||||
if (currentIndex.value === 0 && offset > 0) { // これ以上戻れない
|
||||
imagesOffset.value = currentScrollLeft + (offset / 3);
|
||||
} else if (currentIndex.value === props.images.length - 1 && offset < 0) { // これ以上進めない
|
||||
imagesOffset.value = currentScrollLeft + (offset / 3);
|
||||
contentsOffset.value = currentScrollLeft + (offset / 3);
|
||||
} else if (currentIndex.value === props.contents.length - 1 && offset < 0) { // これ以上進めない
|
||||
contentsOffset.value = currentScrollLeft + (offset / 3);
|
||||
} else {
|
||||
imagesOffset.value = currentScrollLeft + offset;
|
||||
contentsOffset.value = currentScrollLeft + offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,14 +102,14 @@ function scrollToCurrentIndex() {
|
||||
const targetOffset = currentIndex.value * -screenWidth.value;
|
||||
currentScrollLeft = targetOffset;
|
||||
|
||||
if (!prefer.s.animation || imagesOffset.value === targetOffset) {
|
||||
if (!prefer.s.animation || contentsOffset.value === targetOffset) {
|
||||
enableSlideTransition.value = false;
|
||||
imagesOffset.value = targetOffset;
|
||||
contentsOffset.value = targetOffset;
|
||||
return;
|
||||
}
|
||||
|
||||
enableSlideTransition.value = true;
|
||||
imagesOffset.value = targetOffset;
|
||||
contentsOffset.value = targetOffset;
|
||||
}
|
||||
|
||||
function onSlideTransitionFinished(ev: TransitionEvent) {
|
||||
@@ -122,7 +122,7 @@ function onCancelHorizontalSwipe() {
|
||||
}
|
||||
|
||||
function onNext() {
|
||||
if (currentIndex.value < props.images.length - 1) {
|
||||
if (currentIndex.value < props.contents.length - 1) {
|
||||
currentIndex.value++;
|
||||
}
|
||||
scrollToCurrentIndex();
|
||||
@@ -140,9 +140,9 @@ function onItemClose() {
|
||||
}
|
||||
|
||||
function onAfterLeave() {
|
||||
for (const image of props.images) {
|
||||
if (image.sourceElement != null) {
|
||||
image.sourceElement.style.visibility = '';
|
||||
for (const content of props.contents) {
|
||||
if (content.sourceElement != null) {
|
||||
content.sourceElement.style.visibility = '';
|
||||
}
|
||||
}
|
||||
emit('closed');
|
||||
@@ -184,7 +184,7 @@ function onAfterLeave() {
|
||||
.items {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: calc(v-bind("screenWidth + 'px'") * v-bind("images.length"));
|
||||
width: calc(v-bind("screenWidth + 'px'") * v-bind("contents.length"));
|
||||
height: 100dvh;
|
||||
overflow: clip;
|
||||
contain: strict;
|
||||
|
||||
@@ -107,8 +107,9 @@ async function openGallery(id?: string) {
|
||||
if (found == null) return null;
|
||||
return markRaw(found);
|
||||
};
|
||||
const images = props.mediaList.filter(media => previewable(media)).map(media => ({
|
||||
const contents = props.mediaList.filter(media => previewable(media)).map(media => ({
|
||||
id: media.id,
|
||||
type: media.type.startsWith('video') ? 'video' : 'image',
|
||||
url: media.url,
|
||||
thumbnailUrl: media.thumbnailUrl,
|
||||
width: media.properties.width ?? 0,
|
||||
@@ -118,8 +119,8 @@ async function openGallery(id?: string) {
|
||||
sourceElement: getElementByMarker(`${markerId}:${media.id}`),
|
||||
}));
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageGallery.vue').then(x => x.default), {
|
||||
defaultIndex: images.findIndex(image => image.id === id),
|
||||
images: images,
|
||||
defaultIndex: contents.findIndex(conten => conten.id === id),
|
||||
contents: contents,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user