feat: アニメーション画像の表示方法を細分化

This commit is contained in:
NoriDev 2023-10-01 02:29:38 +09:00
parent 54e4995bff
commit 7da40193b8
16 changed files with 242 additions and 20 deletions

View file

@ -28,6 +28,9 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGELOG.md#2023xx) 문서를 참고하십시오.
### Client
- Feat: 움직이는 이미지를 표시하는 방법을 세분화
- 마우스를 움직이거나 화면을 터치하고 있으면 이미지를 재생
- 일정 시간이 경과하면 이미지 재생을 중지
- Fix: 로그인하지 않은 상태에서 노트 상세 페이지의 노트 작성 폼을 조작할 수 있음
---

View file

@ -1,5 +1,6 @@
---
_lang_: "English"
showingAnimatedImagesDescription: "When set to \"Animate on interaction\", the image will play when you hover over it or touch it."
showFixedPostFormInReplies: "Show posting form in replies"
showFixedPostFormInRepliesDescription: "Only visible in desktop and tablet environments."
renoteQuoteButtonSeparation: "Show renote and quote buttons separately"
@ -1205,6 +1206,10 @@ additionalPermissionsForFlash: "Allow to add permission to Play"
thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions"
doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?"
translateProfile: "Translate profile"
_showingAnimatedImages:
always: "Always animate"
interaction: "Animate on interaction"
inactive: "Stop after a certain amount of time"
_messaging:
direct: "Direct Message"
_tlTutorial:

6
locales/index.d.ts vendored
View file

@ -3,6 +3,7 @@
// Do not edit this file directly.
export interface Locale {
"_lang_": string;
"showingAnimatedImagesDescription": string;
"showFixedPostFormInReplies": string;
"showFixedPostFormInRepliesDescription": string;
"renoteQuoteButtonSeparation": string;
@ -1211,6 +1212,11 @@ export interface Locale {
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
"translateProfile": string;
"_showingAnimatedImages": {
"always": string;
"interaction": string;
"inactive": string;
};
"_messaging": {
"direct": string;
};

View file

@ -1,5 +1,6 @@
_lang_: "日本語"
showingAnimatedImagesDescription: "「インタラクト時に再生」に設定すると、画像の上にマウスを置いたり、画像をタッチすると再生されます。"
showFixedPostFormInReplies: "返信に投稿フォームを表示する"
showFixedPostFormInRepliesDescription: "デスクトップとタブレット環境でのみ表示されます。"
renoteQuoteButtonSeparation: "リノートと引用ボタンを分けて表示する"
@ -1209,6 +1210,11 @@ thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか"
translateProfile: "プロフィールを翻訳する"
_showingAnimatedImages:
always: "常に再生"
interaction: "インタラクト時に再生"
inactive: "一定時間経過すると再生"
_messaging:
direct: "ダイレクトメッセージ"

View file

@ -1,5 +1,6 @@
---
_lang_: "한국어"
showingAnimatedImagesDescription: "'건드리면 움직임'으로 설정하면 이미지 위에 마우스를 올리거나 이미지를 터치하면 움직여요."
showFixedPostFormInReplies: "답글에 글 작성란 표시"
showFixedPostFormInRepliesDescription: "데스크톱과 태블릿 환경에서만 표시돼요."
renoteQuoteButtonSeparation: "리노트와 인용 버튼을 분리해서 표시하기"
@ -1191,6 +1192,10 @@ additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
translateProfile: "프로필 번역하기"
_showingAnimatedImages:
always: "항상 움직임"
interaction: "건드리면 움직임"
inactive: "일정 시간이 지나면 멈춤"
_messaging:
direct: "다이렉트 메시지"
_tlTutorial:

View file

@ -27,6 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:width="image.properties.width"
:height="image.properties.height"
:style="hide ? 'filter: brightness(0.7);' : null"
@mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
@mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"
/>
</component>
<template v-if="hide">
@ -51,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { onMounted, onUnmounted, watch } from 'vue';
import * as Misskey from 'cherrypick-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import bytes from '@/filters/bytes.js';
@ -76,9 +78,12 @@ const props = withDefaults(defineProps<{
let hide = $ref(true);
let darkMode: boolean = $ref(defaultStore.state.darkMode);
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url
: defaultStore.state.disableShowingAnimatedImages
: defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(props.image.url)
: props.image.thumbnailUrl,
);
@ -92,6 +97,12 @@ function onclick() {
}
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
// Plugin:register_note_view_interruptor 使watch
watch(() => props.image, () => {
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
@ -117,6 +128,21 @@ function showMenu(ev: MouseEvent) {
}] : [])], ev.currentTarget ?? ev.target);
}
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -40,6 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="!disableShowingAnimatedImages" style="margin-bottom: 15px;" warn>{{ i18n.ts.photosensitiveSeizuresWarning }}</MkInfo>
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}<template #caption>{{ i18n.ts.disableShowingAnimatedImagesDescription }}</template></MkSwitch>
<MkRadios v-if="!disableShowingAnimatedImages" v-model="showingAnimatedImages" style="margin-left: 44px;">
<option value="always">{{ i18n.ts._showingAnimatedImages.always }}</option>
<option value="interaction">{{ i18n.ts._showingAnimatedImages.interaction }}</option>
<option value="inactive">{{ i18n.ts._showingAnimatedImages.inactive }}</option>
<template #caption>{{ i18n.ts.showingAnimatedImagesDescription }}</template>
</MkRadios>
</MkFolder>
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
@ -52,12 +58,14 @@ import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkRadios from '@/components/MkRadios.vue';
import { defaultStore } from '@/store.js';
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const showingAnimatedImages = computed(defaultStore.makeGetterSetter('showingAnimatedImages'));
</script>
<style lang="scss" module>

View file

@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="$style.root" :style="{ color }" :title="acct(user)" @click="onClick">
<MkImgWithBlurhash :class="[$style.inner, { [$style.reduceBlurEffect]: !defaultStore.state.useBlurEffect, [$style.noDrag]: noDrag }]" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true" :noDrag="true"/>
<MkImgWithBlurhash :class="[$style.inner, { [$style.reduceBlurEffect]: !defaultStore.state.useBlurEffect, [$style.noDrag]: noDrag }]" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true" :noDrag="true" @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"/>
</component>
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { onMounted, onUnmounted, watch } from 'vue';
import * as Misskey from 'cherrypick-js';
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import MkA from '@/components/global/MkA.vue';
@ -38,7 +38,10 @@ const bound = $computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@ -47,6 +50,12 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
let color = $ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
@ -54,6 +63,22 @@ watch(() => props.user.avatarBlurhash, () => {
}, {
immediate: true,
});
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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">
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true" @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { onMounted, onUnmounted, watch } from 'vue';
import * as Misskey from 'cherrypick-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
@ -62,7 +62,10 @@ const bound = $computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@ -71,6 +74,12 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
let color = $ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
@ -78,6 +87,22 @@ watch(() => props.user.avatarBlurhash, () => {
}, {
immediate: true,
});
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<span v-if="errored">:{{ customEmojiName }}:</span>
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false" @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"/>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
@ -36,6 +36,9 @@ const rawUrl = computed(() => {
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const url = computed(() => {
if (rawUrl.value == null) return null;
@ -48,13 +51,35 @@ const url = computed(() => {
false,
true,
);
return defaultStore.reactiveState.disableShowingAnimatedImages.value
return defaultStore.reactiveState.disableShowingAnimatedImages.value || (['interaction', 'inactive'].includes(<string>defaultStore.reactiveState.showingAnimatedImages.value) && !playAnimation)
? getStaticImageUrl(proxied)
: proxied;
});
const alt = computed(() => `:${customEmojiName.value}:`);
let errored = $ref(url.value == null);
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="$style.root" :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" :onlyAvgColor="true" @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"/>
</component>
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { onMounted, onUnmounted, watch } from 'vue';
import * as Misskey from 'cherrypick-js';
import MkA from '@/components/global/MkA.vue';
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
import { acct, userPage } from '@/filters/user.js';
@ -37,7 +38,10 @@ const bound = $computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@ -46,6 +50,12 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
let color = $ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
@ -53,6 +63,22 @@ watch(() => props.user.avatarBlurhash, () => {
}, {
immediate: true,
});
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -131,6 +131,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</MkSwitch>
<MkSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</MkSwitch>
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}<template #caption>{{ i18n.ts.disableShowingAnimatedImagesDescription }}</template></MkSwitch>
<MkSelect v-if="!disableShowingAnimatedImages" v-model="showingAnimatedImages" style="margin-left: 44px;">
<option value="always">{{ i18n.ts._showingAnimatedImages.always }}</option>
<option value="interaction">{{ i18n.ts._showingAnimatedImages.interaction }}</option>
<option value="inactive">{{ i18n.ts._showingAnimatedImages.inactive }}</option>
<template #caption>{{ i18n.ts.showingAnimatedImagesDescription }}</template>
</MkSelect>
<MkSwitch v-model="highlightSensitiveMedia">{{ i18n.ts.highlightSensitiveMedia }}</MkSwitch>
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
<MkSwitch v-model="hideAvatarsInNote">{{ i18n.ts.hideAvatarsInNote }} <span class="_beta">CherryPick</span></MkSwitch>
@ -359,6 +365,7 @@ const infoButtonForNoteActionsEnabled = computed(defaultStore.makeGetterSetter('
const showReplyInNotification = computed(defaultStore.makeGetterSetter('showReplyInNotification'));
const renoteQuoteButtonSeparation = computed(defaultStore.makeGetterSetter('renoteQuoteButtonSeparation'));
const showFixedPostFormInReplies = computed(defaultStore.makeGetterSetter('showFixedPostFormInReplies'));
const showingAnimatedImages = computed(defaultStore.makeGetterSetter('showingAnimatedImages'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@ -415,6 +422,7 @@ watch([
showReplyInNotification,
renoteQuoteButtonSeparation,
showFixedPostFormInReplies,
showingAnimatedImages,
], async () => {
await reloadAsk();
});

View file

@ -71,6 +71,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'loadRawImages',
'imageNewTab',
'disableShowingAnimatedImages',
'showingAnimatedImages',
'emojiStyle',
'disableDrawer',
'useBlurEffectForModal',

View file

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.img"
:to="notePage(image.note)"
>
<ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name"/>
<ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name" @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"/>
</MkA>
</div>
<p v-if="!fetching && images.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import * as Misskey from 'cherrypick-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { notePage } from '@/filters/note.js';
@ -45,13 +45,28 @@ let images = $ref<{
file: Misskey.entities.DriveFile;
}[]>([]);
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
function thumbnail(image: Misskey.entities.DriveFile): string {
return defaultStore.state.disableShowingAnimatedImages
return defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(image.url)
: image.thumbnailUrl;
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
const image = [
'image/jpeg',
'image/webp',
@ -78,6 +93,14 @@ onMounted(() => {
fetching = false;
});
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
});
</script>
<style lang="scss" module>

View file

@ -234,6 +234,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
showingAnimatedImages: {
where: 'device',
default: 'always' as 'always' | 'interaction' | 'inactive',
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native

View file

@ -15,6 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-for="(image, i) in images" :key="i"
:class="$style.img"
:style="`background-image: url(${thumbnail(image)})`"
@mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
@mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"
></div>
</div>
</div>
@ -22,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue';
import { onMounted, onUnmounted, ref } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { useStream } from '@/stream.js';
@ -67,12 +69,21 @@ const onDriveFileCreated = (file) => {
}
};
let playAnimation = $ref(true);
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
const thumbnail = (image: any): string => {
return defaultStore.state.disableShowingAnimatedImages
return defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
? getStaticImageUrl(image.url)
: image.thumbnailUrl;
};
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
}
os.api('drive/stream', {
type: 'image/*',
limit: 9,
@ -82,7 +93,22 @@ os.api('drive/stream', {
});
connection.on('driveFileCreated', onDriveFileCreated);
onMounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.addEventListener('mousemove', resetTimer);
window.addEventListener('touchstart', resetTimer);
window.addEventListener('touchend', resetTimer);
}
});
onUnmounted(() => {
if (defaultStore.state.showingAnimatedImages === 'inactive') {
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('touchstart', resetTimer);
window.removeEventListener('touchend', resetTimer);
}
connection.dispose();
});