feat(client): データセーバーモードの追加 (#10478)
* change nsfw settings
* Update CHANGELOG.md
* (fix) eliminate warning message when manually hide
* Apply suggestions from code review
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
* (change) translation key
* revert nsfw settings (partial)
* (add) data saver setting
* Integrate MkMediaBlurhash and MkImgWithBlurhash
* Update CHANGELOG.md
* 🎨
* リモートのファイルでsizeが0の場合は表示しない, refを作らない
* fix
* かっこ
---------
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
parent
98383b2aa9
commit
bcbf06ac8c
7 changed files with 28 additions and 9 deletions
|
@ -118,6 +118,8 @@
|
||||||
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
||||||
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
||||||
- Add Minimizing ("folding") of windows
|
- Add Minimizing ("folding") of windows
|
||||||
|
- 「データセーバー」モードを追加
|
||||||
|
- 非NSFWメディアが隠れている際にも「閲覧注意」が出てしまう問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- PostgreSQLのレプリケーション対応
|
- PostgreSQLのレプリケーション対応
|
||||||
|
|
|
@ -271,6 +271,7 @@ home: "ホーム"
|
||||||
remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
|
remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
|
||||||
activity: "アクティビティ"
|
activity: "アクティビティ"
|
||||||
images: "画像"
|
images: "画像"
|
||||||
|
image: "画像"
|
||||||
birthday: "誕生日"
|
birthday: "誕生日"
|
||||||
yearsOld: "{age}歳"
|
yearsOld: "{age}歳"
|
||||||
registeredDate: "登録日"
|
registeredDate: "登録日"
|
||||||
|
@ -990,6 +991,9 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを
|
||||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
largeNoteReactions: "ノートのリアクションを大きく表示"
|
||||||
noteIdOrUrl: "ノートIDまたはURL"
|
noteIdOrUrl: "ノートIDまたはURL"
|
||||||
|
video: "動画"
|
||||||
|
videos: "動画"
|
||||||
|
dataSaver: "データセーバー"
|
||||||
accountMigration: "アカウントの引っ越し"
|
accountMigration: "アカウントの引っ越し"
|
||||||
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
||||||
forceShowAds: "常に広告を表示する"
|
forceShowAds: "常に広告を表示する"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title">
|
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title">
|
||||||
<canvas v-if="!loaded" ref="canvas" :class="$style.canvas" :width="size" :height="size" :title="title"/>
|
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="size" :height="size" :title="title"/>
|
||||||
<img v-if="src" :class="$style.img" :src="src" :title="title" :alt="alt" @load="onLoad"/>
|
<img v-if="src && !forceBlurhash" :class="$style.img" :src="src" :title="title" :alt="alt" @load="onLoad"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,16 +12,18 @@ import { decode } from 'blurhash';
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
src?: string | null;
|
src?: string | null;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
alt?: string;
|
alt?: string | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
size?: number;
|
size?: number;
|
||||||
cover?: boolean;
|
cover?: boolean;
|
||||||
|
forceBlurhash?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
src: null,
|
src: null,
|
||||||
alt: '',
|
alt: '',
|
||||||
title: null,
|
title: null,
|
||||||
size: 64,
|
size: 64,
|
||||||
cover: true,
|
cover: true,
|
||||||
|
forceBlurhash: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvas = $shallowRef<HTMLCanvasElement>();
|
const canvas = $shallowRef<HTMLCanvasElement>();
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :force-blurhash="defaultStore.state.enableDataSaverMode" />
|
||||||
<div :class="$style.hiddenText">
|
<div :class="$style.hiddenText">
|
||||||
<div :class="$style.hiddenTextWrapper">
|
<div :class="$style.hiddenTextWrapper">
|
||||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}</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>
|
||||||
|
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -38,7 +40,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
let darkMode = $ref(defaultStore.state.darkMode);
|
let darkMode: boolean = $ref(defaultStore.state.darkMode);
|
||||||
|
|
||||||
const url = (props.raw || defaultStore.state.loadRawImages)
|
const url = (props.raw || defaultStore.state.loadRawImages)
|
||||||
? props.image.url
|
? props.image.url
|
||||||
|
@ -48,7 +50,7 @@ const url = (props.raw || defaultStore.state.loadRawImages)
|
||||||
|
|
||||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||||
watch(() => props.image, () => {
|
watch(() => props.image, () => {
|
||||||
hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore');
|
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||||
}, {
|
}, {
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
|
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
|
||||||
|
<!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
|
||||||
<div>
|
<div>
|
||||||
<b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}</b>
|
<b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
|
||||||
|
<b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||||
<span>{{ i18n.ts.clickToShow }}</span>
|
<span>{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,6 +27,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
import VuePlyr from 'vue-plyr';
|
import VuePlyr from 'vue-plyr';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import 'vue-plyr/dist/vue-plyr.css';
|
import 'vue-plyr/dist/vue-plyr.css';
|
||||||
|
@ -34,7 +37,7 @@ const props = defineProps<{
|
||||||
video: misskey.entities.DriveFile;
|
video: misskey.entities.DriveFile;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSensitive && (defaultStore.state.nsfw !== 'ignore'));
|
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
||||||
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
||||||
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MkRadios v-model="emojiStyle">
|
<MkRadios v-model="emojiStyle">
|
||||||
|
@ -160,6 +161,7 @@ const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||||
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
||||||
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
|
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
|
||||||
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
||||||
|
const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode'));
|
||||||
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
||||||
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
||||||
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
|
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
|
||||||
|
|
|
@ -182,6 +182,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableDataSaverMode: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
disableShowingAnimatedImages: {
|
disableShowingAnimatedImages: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: matchMedia('(prefers-reduced-motion)').matches,
|
default: matchMedia('(prefers-reduced-motion)').matches,
|
||||||
|
|
Loading…
Reference in a new issue