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:
かっこかり 2023-04-15 15:29:57 +09:00 committed by GitHub
parent 98383b2aa9
commit bcbf06ac8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 28 additions and 9 deletions

View file

@ -118,6 +118,8 @@
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように - 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります - 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
- Add Minimizing ("folding") of windows - Add Minimizing ("folding") of windows
- 「データセーバー」モードを追加
- 非NSFWメディアが隠れている際にも「閲覧注意」が出てしまう問題を修正
### Server ### Server
- PostgreSQLのレプリケーション対応 - PostgreSQLのレプリケーション対応

View file

@ -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: "常に広告を表示する"

View file

@ -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>();

View file

@ -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,

View file

@ -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>

View file

@ -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'));

View file

@ -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,