wip ( 絵文字ミュートの基礎実装, PoC )
This commit is contained in:
parent
d476f7ff50
commit
6384f03c8e
|
@ -5417,6 +5417,18 @@ export interface Locale extends ILocale {
|
||||||
* スクロールして閉じる
|
* スクロールして閉じる
|
||||||
*/
|
*/
|
||||||
"scrollToClose": string;
|
"scrollToClose": string;
|
||||||
|
/**
|
||||||
|
* 絵文字ミュート
|
||||||
|
*/
|
||||||
|
"emojiMute": string;
|
||||||
|
/**
|
||||||
|
* {x}をミュート
|
||||||
|
*/
|
||||||
|
"muteX": ParameterizedString<"x">;
|
||||||
|
/**
|
||||||
|
* {x}のミュートを解除
|
||||||
|
*/
|
||||||
|
"unmuteX": ParameterizedString<"x">;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
|
|
|
@ -1349,6 +1349,9 @@ goToDeck: "デッキへ戻る"
|
||||||
federationJobs: "連合ジョブ"
|
federationJobs: "連合ジョブ"
|
||||||
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
||||||
scrollToClose: "スクロールして閉じる"
|
scrollToClose: "スクロールして閉じる"
|
||||||
|
emojiMute: "絵文字ミュート"
|
||||||
|
muteX: "{x}をミュート"
|
||||||
|
unmuteX: "{x}のミュートを解除"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "まだメッセージはありません"
|
noMessagesYet: "まだメッセージはありません"
|
||||||
|
|
|
@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img
|
<img
|
||||||
v-if="errored && fallbackToImage"
|
v-if="(errored || shouldMute ) && fallbackToImage"
|
||||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||||
src="/client-assets/dummy.png"
|
src="/client-assets/dummy.png"
|
||||||
:title="alt"
|
:title="alt"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
style="-webkit-user-drag: none;"
|
style="-webkit-user-drag: none;"
|
||||||
/>
|
/>
|
||||||
<span v-else-if="errored">:{{ customEmojiName }}:</span>
|
<span v-else-if="errored || shouldMute">:{{ customEmojiName }}:</span>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||||
|
@ -51,12 +51,16 @@ const props = defineProps<{
|
||||||
menu?: boolean;
|
menu?: boolean;
|
||||||
menuReaction?: boolean;
|
menuReaction?: boolean;
|
||||||
fallbackToImage?: boolean;
|
fallbackToImage?: boolean;
|
||||||
|
ignoreMuted?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const react = inject(DI.mfmEmojiReactCallback);
|
const react = inject(DI.mfmEmojiReactCallback);
|
||||||
|
|
||||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||||
|
const emojiCodeToMute = props.name.startsWith(':') ? props.name : `:${props.name}${props.host ? `@${props.host}` : ''}:`;
|
||||||
|
const isMuted = computed(() => prefer.r.mutingEmojis.value.includes(emojiCodeToMute));
|
||||||
|
const shouldMute = computed(() => !props.ignoreMuted && isMuted.value);
|
||||||
|
|
||||||
const rawUrl = computed(() => {
|
const rawUrl = computed(() => {
|
||||||
if (props.url) {
|
if (props.url) {
|
||||||
|
@ -95,14 +99,18 @@ function onClick(ev: MouseEvent) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: `:${props.name}:`,
|
text: `:${props.name}:`,
|
||||||
}, {
|
|
||||||
text: i18n.ts.copy,
|
|
||||||
icon: 'ti ti-copy',
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(`:${props.name}:`);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isLocal.value) {
|
||||||
|
menuItems.push({
|
||||||
|
text: i18n.ts.copy,
|
||||||
|
icon: 'ti ti-copy',
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`:${props.name}:`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (props.menuReaction && react) {
|
if (props.menuReaction && react) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
text: i18n.ts.doReaction,
|
text: i18n.ts.doReaction,
|
||||||
|
@ -113,21 +121,33 @@ function onClick(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLocal.value) {
|
||||||
|
menuItems.push({
|
||||||
|
type: 'divider',
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.info,
|
||||||
|
icon: 'ti ti-info-circle',
|
||||||
|
action: async () => {
|
||||||
|
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
|
||||||
|
emoji: await misskeyApiGet('emoji', {
|
||||||
|
name: customEmojiName.value,
|
||||||
|
}),
|
||||||
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
text: i18n.ts.info,
|
text: i18n.ts.mute,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-mood-off',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
|
await mute();
|
||||||
emoji: await misskeyApiGet('emoji', {
|
|
||||||
name: customEmojiName.value,
|
|
||||||
}),
|
|
||||||
}, {
|
|
||||||
closed: () => dispose(),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($i?.isModerator ?? $i?.isAdmin) {
|
if (($i?.isModerator ?? $i?.isAdmin) && isLocal.value) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
text: i18n.ts.edit,
|
text: i18n.ts.edit,
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
|
@ -152,6 +172,21 @@ async function edit(name: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mute() {
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.tsx.muteX({ x: emojiCodeToMute }),
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
if (!mutedEmojis.includes(emojiCodeToMute)) {
|
||||||
|
prefer.commit('mutingEmojis', [...mutedEmojis, emojiCodeToMute]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
<img v-if="shouldMute" :class="$style.root" src="/client-assets/dummy.png" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||||
|
<img v-else-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ const props = defineProps<{
|
||||||
emoji: string;
|
emoji: string;
|
||||||
menu?: boolean;
|
menu?: boolean;
|
||||||
menuReaction?: boolean;
|
menuReaction?: boolean;
|
||||||
|
ignoreMuted?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const react = inject(DI.mfmEmojiReactCallback, null);
|
const react = inject(DI.mfmEmojiReactCallback, null);
|
||||||
|
@ -32,12 +34,46 @@ const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : cha
|
||||||
const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
|
const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
|
||||||
const url = computed(() => char2path(props.emoji));
|
const url = computed(() => char2path(props.emoji));
|
||||||
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
|
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
|
||||||
|
const isMuted = computed(() => prefer.r.mutingEmojis.value.includes(props.emoji));
|
||||||
|
const shouldMute = computed(() => isMuted.value && !props.ignoreMuted);
|
||||||
|
|
||||||
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
||||||
function computeTitle(event: PointerEvent): void {
|
function computeTitle(event: PointerEvent): void {
|
||||||
(event.target as HTMLElement).title = getEmojiName(props.emoji);
|
(event.target as HTMLElement).title = getEmojiName(props.emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mute() {
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.tsx.muteX({ x: props.emoji }),
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
if (!mutedEmojis.includes(props.emoji)) {
|
||||||
|
prefer.commit('mutingEmojis', [...mutedEmojis, props.emoji]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmute() {
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.tsx.unmuteX({ x: props.emoji }),
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
const index = mutedEmojis.indexOf(props.emoji);
|
||||||
|
if (index !== -1) {
|
||||||
|
mutedEmojis.splice(index, 1);
|
||||||
|
prefer.commit('mutingEmojis', mutedEmojis);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onClick(ev: MouseEvent) {
|
function onClick(ev: MouseEvent) {
|
||||||
if (props.menu) {
|
if (props.menu) {
|
||||||
const menuItems: MenuItem[] = [];
|
const menuItems: MenuItem[] = [];
|
||||||
|
@ -63,6 +99,22 @@ function onClick(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menuItems.push({
|
||||||
|
type: 'divider',
|
||||||
|
}, isMuted.value ? {
|
||||||
|
text: i18n.ts.unmute,
|
||||||
|
icon: 'ti ti-mood-smile',
|
||||||
|
action: () => {
|
||||||
|
unmute();
|
||||||
|
},
|
||||||
|
} : {
|
||||||
|
text: i18n.ts.mute,
|
||||||
|
icon: 'ti ti-mood-off',
|
||||||
|
action: () => {
|
||||||
|
mute();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -435,6 +435,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
normal: props.plain,
|
normal: props.plain,
|
||||||
host: props.author.host,
|
host: props.author.host,
|
||||||
useOriginalSize: scale >= 2.5,
|
useOriginalSize: scale >= 2.5,
|
||||||
|
menu: props.enableEmojiMenu,
|
||||||
|
menuReaction: false,
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.emojis">
|
||||||
|
<div v-for="emoji in emojis" :key="`emojiMute-${emoji}`" :class="$style.emoji" @click="onEmojiClick($event, emoji)">
|
||||||
|
<MkCustomEmoji
|
||||||
|
v-if="emoji.startsWith(':')"
|
||||||
|
:name="customEmojiName(emoji)"
|
||||||
|
:host="customEmojiHost(emoji)"
|
||||||
|
:normal="true"
|
||||||
|
:menu="false"
|
||||||
|
:menuReaction="false"
|
||||||
|
:ignoreMuted="true"
|
||||||
|
/>
|
||||||
|
<MkEmoji
|
||||||
|
v-else
|
||||||
|
:emoji="emoji"
|
||||||
|
:menu="false"
|
||||||
|
:menuReaction="false"
|
||||||
|
:ignoreMuted="true"
|
||||||
|
></MkEmoji>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MkButton primary inline @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MenuItem } from '@/types/menu';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
|
const emojis = prefer.model('mutingEmojis');
|
||||||
|
|
||||||
|
function customEmojiName (name:string) {
|
||||||
|
return (name[0] === ':' ? name.substring(1, name.length - 1) : name).replace('@.', '').split('@')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function customEmojiHost (name:string) {
|
||||||
|
// nameは:emojiName@host:の形式
|
||||||
|
// 取り出したい部分はhostなので、@以降を取り出す
|
||||||
|
const index = name.indexOf('@');
|
||||||
|
if (index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return name.substring(index + 1, name.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHTMLElement(ev: MouseEvent): HTMLElement {
|
||||||
|
const target = ev.currentTarget ?? ev.target;
|
||||||
|
return target as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mute(emoji: string) {
|
||||||
|
const emojiCodeToMute = emoji.startsWith(':') ? emoji : `:${emoji}:`;
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.tsx.muteX({ x: emojiCodeToMute }),
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
if (!mutedEmojis.includes(emojiCodeToMute)) {
|
||||||
|
prefer.commit('mutingEmojis', [...mutedEmojis, emojiCodeToMute]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(ev: MouseEvent) {
|
||||||
|
os.pickEmoji(getHTMLElement(ev), { showPinned: false }).then((emoji) => {
|
||||||
|
if (emoji) {
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
if (!mutedEmojis.includes(emoji)) {
|
||||||
|
prefer.commit('mutingEmojis', [...mutedEmojis, emoji]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEmojiClick(ev: MouseEvent, emoji: string) {
|
||||||
|
const menuItems : MenuItem[] = [{
|
||||||
|
type: 'label',
|
||||||
|
text: emoji,
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.unmute,
|
||||||
|
icon: 'ti ti-mood-off',
|
||||||
|
action: () => unmute(emoji),
|
||||||
|
}];
|
||||||
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmute(emoji: string) {
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.tsx.unmuteX({ x: emoji }),
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||||
|
if (mutedEmojis.includes(emoji)) {
|
||||||
|
prefer.commit('mutingEmojis', mutedEmojis.filter((e) => e !== emoji));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style module>
|
||||||
|
.emojis {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-radius: 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--MI_THEME-buttonBg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -49,6 +49,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
<SearchMarker
|
||||||
|
:label="i18n.ts.emojiMute"
|
||||||
|
:keywords="['emoji', 'mute', 'hide']"
|
||||||
|
>
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-mood-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.emojiMute }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<XEmojiMute/>
|
||||||
|
</div>
|
||||||
|
</mkfolder>
|
||||||
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker
|
<SearchMarker
|
||||||
:label="i18n.ts.instanceMute"
|
:label="i18n.ts.instanceMute"
|
||||||
:keywords="['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide']"
|
:keywords="['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide']"
|
||||||
|
@ -178,6 +192,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import XEmojiMute from './mute-block.emoji-mute.vue';
|
||||||
import XInstanceMute from './mute-block.instance-mute.vue';
|
import XInstanceMute from './mute-block.instance-mute.vue';
|
||||||
import XWordMute from './mute-block.word-mute.vue';
|
import XWordMute from './mute-block.word-mute.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
|
|
@ -342,6 +342,9 @@ export const PREF_DEF = {
|
||||||
plugins: {
|
plugins: {
|
||||||
default: [] as Plugin[],
|
default: [] as Plugin[],
|
||||||
},
|
},
|
||||||
|
mutingEmojis: {
|
||||||
|
default: [] as string[],
|
||||||
|
},
|
||||||
|
|
||||||
'sound.masterVolume': {
|
'sound.masterVolume': {
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
|
|
Loading…
Reference in New Issue