Skip to content

Commit

Permalink
feat: アニメーション画像の表示方法を細分化
Browse files Browse the repository at this point in the history
  • Loading branch information
noridev committed Sep 30, 2023
1 parent 54e4995 commit 7da4019
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 20 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_CHERRYPICK.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGELOG.md#2023xx) 문서를 참고하십시오.

### Client
- Feat: 움직이는 이미지를 표시하는 방법을 세분화
- 마우스를 움직이거나 화면을 터치하고 있으면 이미지를 재생
- 일정 시간이 경과하면 이미지 재생을 중지
- Fix: 로그인하지 않은 상태에서 노트 상세 페이지의 노트 작성 폼을 조작할 수 있음

---
Expand Down
5 changes: 5 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Do not edit this file directly.
export interface Locale {
"_lang_": string;
"showingAnimatedImagesDescription": string;
"showFixedPostFormInReplies": string;
"showFixedPostFormInRepliesDescription": string;
"renoteQuoteButtonSeparation": string;
Expand Down Expand Up @@ -1211,6 +1212,11 @@ export interface Locale {
"thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string;
"translateProfile": string;
"_showingAnimatedImages": {
"always": string;
"interaction": string;
"inactive": string;
};
"_messaging": {
"direct": string;
};
Expand Down
6 changes: 6 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
_lang_: "日本語"

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

_showingAnimatedImages:
always: "常に再生"
interaction: "インタラクト時に再生"
inactive: "一定時間経過すると再生"

_messaging:
direct: "ダイレクトメッセージ"

Expand Down
5 changes: 5 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
_lang_: "한국어"
showingAnimatedImagesDescription: "'건드리면 움직임'으로 설정하면 이미지 위에 마우스를 올리거나 이미지를 터치하면 움직여요."
showFixedPostFormInReplies: "답글에 글 작성란 표시"
showFixedPostFormInRepliesDescription: "데스크톱과 태블릿 환경에서만 표시돼요."
renoteQuoteButtonSeparation: "리노트와 인용 버튼을 분리해서 표시하기"
Expand Down Expand Up @@ -1191,6 +1192,10 @@ additionalPermissionsForFlash: "Play에 대한 추가 권한"
thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요"
doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?"
translateProfile: "프로필 번역하기"
_showingAnimatedImages:
always: "항상 움직임"
interaction: "건드리면 움직임"
inactive: "일정 시간이 지나면 멈춤"
_messaging:
direct: "다이렉트 메시지"
_tlTutorial:
Expand Down
32 changes: 29 additions & 3 deletions packages/frontend/src/components/MkMediaImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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';
Expand All @@ -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,
);
Expand All @@ -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');
Expand All @@ -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>
Expand All @@ -126,7 +152,7 @@ function showMenu(ev: MouseEvent) {

.sensitive {
position: relative;

&::after {
content: "";
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
31 changes: 28 additions & 3 deletions packages/frontend/src/components/global/CPAvatar-Friendly.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -47,13 +50,35 @@ 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, () => {
color = extractAvgColorFromBlurhash(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>
Expand Down
31 changes: 28 additions & 3 deletions packages/frontend/src/components/global/MkAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -71,13 +74,35 @@ 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, () => {
color = extractAvgColorFromBlurhash(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>
Expand Down
31 changes: 28 additions & 3 deletions packages/frontend/src/components/global/MkCustomEmoji.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand All @@ -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>
Expand Down
Loading

0 comments on commit 7da4019

Please sign in to comment.