Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-11-27 19:58:56 +09:00
commit daff64de4e
9 changed files with 337 additions and 59 deletions

View file

@ -24,6 +24,7 @@
- Enhance: 絵文字のオートコンプリート機能強化 #12364 - Enhance: 絵文字のオートコンプリート機能強化 #12364
- Enhance: ユーザーのRawデータを表示するページが復活 - Enhance: ユーザーのRawデータを表示するページが復活
- Enhance: リアクション選択時に音を鳴らせるように - Enhance: リアクション選択時に音を鳴らせるように
- Enhance: サウンドにドライブのファイルを使用できるように
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
- Fix: コードエディタが正しく表示されない問題を修正 - Fix: コードエディタが正しく表示されない問題を修正

View file

@ -43,7 +43,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
- Revert: 사용자 통계 표시 기능 제거 ([MisskeyIO/misskey@114c7fe6](https://github.com/MisskeyIO/misskey/commit/114c7fe6b37dd6bddbcd9d92406f8b13bf688e8b)) - Revert: 사용자 통계 표시 기능 제거 ([MisskeyIO/misskey@114c7fe6](https://github.com/MisskeyIO/misskey/commit/114c7fe6b37dd6bddbcd9d92406f8b13bf688e8b))
### Client ### Client
- Enhance: 사운드 설정을 저장할 때 상태를 확실하게 표시함 - Enhance: 사운드 설정을 기본값으로 복원하거나 저장할 때 확실하게 표시함
- Enhance: 리모트 서버와 동일한 이모지가 존재하지 않는 경우 '이모지 복사'를 비활성화함 - Enhance: 리모트 서버와 동일한 이모지가 존재하지 않는 경우 '이모지 복사'를 비활성화함
- Enhance: 아이콘 장식을 바로 업로드 하거나 드라이브에서 불러올 수 있음 ([Secineralyr/misskey.dream@e358212d](https://github.com/Secineralyr/misskey.dream/commit/e358212da93256749e31d9e0ca9dd2ed37fd548e), [Secineralyr/misskey.dream@52592fea](https://github.com/Secineralyr/misskey.dream/commit/52592fea52684497ba7e07f173aac2b1083afcb1)) - Enhance: 아이콘 장식을 바로 업로드 하거나 드라이브에서 불러올 수 있음 ([Secineralyr/misskey.dream@e358212d](https://github.com/Secineralyr/misskey.dream/commit/e358212da93256749e31d9e0ca9dd2ed37fd548e), [Secineralyr/misskey.dream@52592fea](https://github.com/Secineralyr/misskey.dream/commit/52592fea52684497ba7e07f173aac2b1083afcb1))
- Fix: '모달 배경색 제거' 옵션이 이모지 피커에 반영되지 않음 - Fix: '모달 배경색 제거' 옵션이 이모지 피커에 반영되지 않음

8
locales/index.d.ts vendored
View file

@ -2258,6 +2258,14 @@ export interface Locale {
"channel": string; "channel": string;
"reaction": string; "reaction": string;
}; };
"_soundSettings": {
"driveFile": string;
"driveFileWarn": string;
"driveFileTypeWarn": string;
"driveFileTypeWarnDescription": string;
"driveFileDurationWarn": string;
"driveFileDurationWarnDescription": string;
};
"_ago": { "_ago": {
"future": string; "future": string;
"justNow": string; "justNow": string;

View file

@ -638,7 +638,7 @@ popout: "ポップアウト"
volume: "音量" volume: "音量"
masterVolume: "マスター音量" masterVolume: "マスター音量"
notUseSound: "サウンドを出力しない" notUseSound: "サウンドを出力しない"
useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する" useSoundOnlyWhenActive: "CherryPickがアクティブな時のみサウンドを出力する"
details: "詳細" details: "詳細"
chooseEmoji: "絵文字を選択" chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません" unableToProcess: "操作を完了できません"
@ -2160,6 +2160,14 @@ _sfx:
channel: "チャンネル通知" channel: "チャンネル通知"
reaction: "リアクション選択時" reaction: "リアクション選択時"
_soundSettings:
driveFile: "ドライブの音声を使用"
driveFileWarn: "ドライブのファイルを選択してください"
driveFileTypeWarn: "このファイルは対応していません"
driveFileTypeWarnDescription: "音声ファイルを選択してください"
driveFileDurationWarn: "音声が長すぎます"
driveFileDurationWarnDescription: "長い音声を使用するとCherryPickの使用に支障をきたす可能性があります。それでも続行しますか"
_ago: _ago:
future: "未来" future: "未来"
justNow: "たった今" justNow: "たった今"

View file

@ -7,24 +7,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<FormSection first> <FormSection first>
<template #label>{{ i18n.ts.sounds }}</template> <template #label>{{ i18n.ts.sounds }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="notUseSound"> <MkSwitch v-model="notUseSound">
<template #label>{{ i18n.ts.notUseSound }}</template> <template #label>{{ i18n.ts.notUseSound }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="useSoundOnlyWhenActive"> <MkSwitch v-model="useSoundOnlyWhenActive">
<template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template> <template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="masterVolume" style="margin-bottom: 25px;" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <MkRange v-model="masterVolume" style="margin-bottom: 25px;" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.masterVolume }}</template> <template #label>{{ i18n.ts.masterVolume }}</template>
</MkRange> </MkRange>
</div> </div>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFolder v-for="type in soundsKeys" :key="type"> <MkFolder v-for="type in operationTypes" :key="type">
<template #label>{{ i18n.t('_sfx.' + type) }}</template> <template #label>{{ i18n.t('_sfx.' + type) }}</template>
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template> <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/> <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
</MkFolder> </MkFolder>
</div> </div>
</FormSection> </FormSection>
@ -48,6 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { Ref, computed, ref, watch } from 'vue'; import { Ref, computed, ref, watch } from 'vue';
import XSound from './sounds.sound.vue'; import XSound from './sounds.sound.vue';
import type { SoundType, OperationType } from '@/scripts/sound.js';
import type { SoundStore } from '@/store.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkRange from '@/components/MkRange.vue'; import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -56,6 +58,7 @@ import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { operationTypes } from '@/scripts/sound.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js'; import { unisonReload } from '@/scripts/unison-reload.js';
@ -63,9 +66,7 @@ const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'))
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive')); const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume')); const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'noteEdited', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const; const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
note: defaultStore.reactiveState.sound_note, note: defaultStore.reactiveState.sound_note,
noteMy: defaultStore.reactiveState.sound_noteMy, noteMy: defaultStore.reactiveState.sound_noteMy,
noteEdited: defaultStore.reactiveState.sound_noteEdited, noteEdited: defaultStore.reactiveState.sound_noteEdited,
@ -94,9 +95,22 @@ async function reloadAsk() {
unisonReload(); unisonReload();
} }
function getSoundTypeName(f: SoundType): string {
switch (f) {
case null:
return i18n.ts.none;
case '_driveFile_':
return i18n.ts._soundSettings.driveFile;
default:
return f;
}
}
async function updated(type: keyof typeof sounds.value, sound) { async function updated(type: keyof typeof sounds.value, sound) {
const v = { const v: SoundStore = {
type: sound.type, type: sound.type,
fileId: sound.fileId,
fileUrl: sound.fileUrl,
volume: sound.volume, volume: sound.volume,
}; };
@ -110,6 +124,8 @@ function reset() {
defaultStore.set(`sound_${sound}`, v); defaultStore.set(`sound_${sound}`, v);
sounds.value[sound] = v; sounds.value[sound] = v;
} }
os.success();
} }
function demoVibrate() { function demoVibrate() {

View file

@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<MkSelect v-model="type"> <MkSelect v-model="type">
<template #label>{{ i18n.ts.sound }}</template> <template #label>{{ i18n.ts.sound }}</template>
<option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option> <option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
</MkSelect> </MkSelect>
<div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
<div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
</div>
<MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.volume }}</template> <template #label>{{ i18n.ts.volume }}</template>
</MkRange> </MkRange>
@ -21,45 +25,162 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { ref, computed } from 'vue';
import type { SoundType } from '@/scripts/sound.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue'; import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { playFile, soundsTypes } from '@/scripts/sound.js'; import * as os from '@/os.js';
import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
import { selectFile } from '@/scripts/select-file.js';
const props = defineProps<{ const props = defineProps<{
type: string; type: SoundType;
fileId?: string;
fileUrl?: string;
volume: number; volume: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update', result: { type: string; volume: number; }): void; (ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void;
}>(); }>();
let type = $ref(props.type); const type = ref<SoundType>(props.type);
let volume = $ref(props.volume); const fileId = ref(props.fileId);
const fileUrl = ref(props.fileUrl);
const fileName = ref<string>('');
const volume = ref(props.volume);
if (type.value === '_driveFile_' && fileId.value) {
const apiRes = await os.api('drive/files/show', {
fileId: fileId.value,
});
fileName.value = apiRes.name;
}
function getSoundTypeName(f: SoundType): string {
switch (f) {
case null:
return i18n.ts.none;
case '_driveFile_':
return i18n.ts._soundSettings.driveFile;
default:
return f;
}
}
const friendlyFileName = computed<string>(() => {
if (fileName.value) {
return fileName.value;
}
if (fileUrl.value) {
return fileUrl.value;
}
return i18n.ts._soundSettings.driveFileWarn;
});
function selectSound(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
if (!file.type.startsWith('audio')) {
os.alert({
type: 'warning',
title: i18n.ts._soundSettings.driveFileTypeWarn,
text: i18n.ts._soundSettings.driveFileTypeWarnDescription,
});
return;
}
const duration = await getSoundDuration(file.url);
if (duration >= 2000) {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts._soundSettings.driveFileDurationWarn,
text: i18n.ts._soundSettings.driveFileDurationWarnDescription,
okText: i18n.ts.continue,
cancelText: i18n.ts.cancel,
});
if (canceled) return;
}
fileUrl.value = file.url;
fileName.value = file.name;
fileId.value = file.id;
});
}
let saved = $ref(false); let saved = $ref(false);
function listen() { function listen() {
playFile(type, volume); if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
os.alert({
type: 'warning',
text: i18n.ts._soundSettings.driveFileWarn,
});
return;
}
playFile(type.value === '_driveFile_' ? {
type: '_driveFile_',
fileId: fileId.value as string,
fileUrl: fileUrl.value as string,
volume: volume.value,
} : {
type: type.value,
volume: volume.value,
});
} }
function save() { function save() {
emit('update', { type, volume }); if (type.value === '_driveFile_' && !fileUrl.value) {
os.alert({
type: 'warning',
text: i18n.ts._soundSettings.driveFileWarn,
});
return;
}
saved = true; if (type.value !== '_driveFile_') {
window.setTimeout(() => { fileUrl.value = undefined;
saved = false; fileName.value = '';
}, 500); fileId.value = undefined;
}
emit('update', {
type: type.value,
fileId: fileId.value,
fileUrl: fileUrl.value,
volume: volume.value,
});
saved = true;
window.setTimeout(() => {
saved = false;
}, 500);
// os.success();
} }
</script> </script>
<style lang="scss" module> <style module>
@keyframes saved { @keyframes saved {
0% { opacity: 1; } 0% { opacity: 1; }
100% { opacity: 0; } 100% { opacity: 0; }
}
.fileSelectorRoot {
display: flex;
align-items: center;
gap: 8px;
}
.fileSelectorButton {
flex-shrink: 0;
}
.fileNotSelected {
font-weight: 700;
color: var(--infoWarnFg);
} }
.saved { .saved {

View file

@ -3,14 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { SoundStore } from '@/store.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
let ctx: AudioContext; let ctx: AudioContext;
const cache = new Map<string, AudioBuffer>(); const cache = new Map<string, AudioBuffer>();
let canPlay = true; let canPlay = true;
export const soundsTypes = [ export const soundsTypes = [
// 音声なし
null, null,
// ドライブの音声
'_driveFile_',
// プリインストール
'syuilo/n-aec', 'syuilo/n-aec',
'syuilo/n-aec-4va', 'syuilo/n-aec-4va',
'syuilo/n-aec-4vb', 'syuilo/n-aec-4vb',
@ -64,32 +72,99 @@ export const soundsTypes = [
'noizenecio/kick_gaba7', 'noizenecio/kick_gaba7',
] as const; ] as const;
export async function loadAudio(file: string, useCache = true) { export const operationTypes = [
'noteMy',
'note',
'noteEdited',
'chat',
'chatBg',
'antenna',
'channel',
'notification',
'reaction',
] as const;
/** サウンドの種類 */
export type SoundType = typeof soundsTypes[number];
/** スプライトの種類 */
export type OperationType = typeof operationTypes[number];
/**
*
* @param soundStore
* @param options `useCache`: `true`
*/
export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
if (_DEV_) console.log('loading audio. opts:', options);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ctx == null) { if (ctx == null) {
ctx = new AudioContext(); ctx = new AudioContext();
} }
if (useCache && cache.has(file)) { if (options?.useCache ?? true) {
return cache.get(file)!; if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
if (_DEV_) console.log('use cache');
return cache.get(soundStore.fileId) as AudioBuffer;
} else if (cache.has(soundStore.type)) {
if (_DEV_) console.log('use cache');
return cache.get(soundStore.type) as AudioBuffer;
}
}
let response: Response;
if (soundStore.type === '_driveFile_') {
try {
response = await fetch(soundStore.fileUrl);
} catch (err) {
try {
// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
const apiRes = await os.api('drive/files/show', {
fileId: soundStore.fileId,
});
response = await fetch(apiRes.url);
} catch (fbErr) {
// それでも無理なら諦める
return;
}
}
} else {
try {
response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
} catch (err) {
return;
}
} }
const response = await fetch(`/client-assets/sounds/${file}.mp3`);
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer); const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
if (useCache) { if (options?.useCache ?? true) {
cache.set(file, audioBuffer); if (soundStore.type === '_driveFile_') {
cache.set(soundStore.fileId, audioBuffer);
} else {
cache.set(soundStore.type, audioBuffer);
}
} }
return audioBuffer; return audioBuffer;
} }
export function play(type: 'noteMy' | 'note' | 'noteEdited' | 'chat' | 'chatBg' | 'antenna' | 'channel' | 'notification' | 'reaction') { /**
const sound = defaultStore.state[`sound_${type}`]; *
if (_DEV_) console.log('play', type, sound); * @param type
*/
export function play(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`];
if (_DEV_) console.log('play', operationType, sound);
if (sound.type == null || !canPlay) return; if (sound.type == null || !canPlay) return;
canPlay = false; canPlay = false;
playFile(sound.type, sound.volume).then(() => { playFile(sound).then(() => {
// ごく短時間に音が重複しないように // ごく短時間に音が重複しないように
setTimeout(() => { setTimeout(() => {
canPlay = true; canPlay = true;
@ -97,9 +172,14 @@ export function play(type: 'noteMy' | 'note' | 'noteEdited' | 'chat' | 'chatBg'
}); });
} }
export async function playFile(file: string, volume: number) { /**
const buffer = await loadAudio(file); *
createSourceNode(buffer, volume)?.start(); * @param soundStore
*/
export async function playFile(soundStore: SoundStore) {
const buffer = await loadAudio(soundStore);
if (!buffer) return;
createSourceNode(buffer, soundStore.volume)?.start();
} }
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
@ -118,6 +198,27 @@ export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBuf
return soundSource; return soundSource;
} }
/**
*
* @param file URLIDではない
*/
export async function getSoundDuration(file: string): Promise<number> {
const audioEl = document.createElement('audio');
audioEl.src = file;
return new Promise((resolve) => {
const si = setInterval(() => {
if (audioEl.readyState > 0) {
resolve(audioEl.duration * 1000);
clearInterval(si);
audioEl.remove();
}
}, 100);
});
}
/**
*
*/
export function isMute(): boolean { export function isMute(): boolean {
if (defaultStore.state.sound_notUseSound) { if (defaultStore.state.sound_notUseSound) {
// サウンドを出力しない // サウンドを出力しない

View file

@ -6,6 +6,7 @@
import { markRaw, ref } from 'vue'; import { markRaw, ref } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import { miLocalStorage } from './local-storage.js'; import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js';
import { Storage } from '@/pizzax.js'; import { Storage } from '@/pizzax.js';
interface PostFormAction { interface PostFormAction {
@ -35,6 +36,22 @@ interface PageViewInterruptor {
handler: (page: Misskey.entities.Page) => unknown; handler: (page: Misskey.entities.Page) => unknown;
} }
/** サウンド設定 */
export type SoundStore = {
type: Exclude<SoundType, '_driveFile_'>;
volume: number;
} | {
type: '_driveFile_';
/** ドライブのファイルID */
fileId: string;
/** ファイルURLこちらが優先される */
fileUrl: string;
volume: number;
}
export const postFormActions: PostFormAction[] = []; export const postFormActions: PostFormAction[] = [];
export const userActions: UserAction[] = []; export const userActions: UserAction[] = [];
export const noteActions: NoteAction[] = []; export const noteActions: NoteAction[] = [];
@ -422,39 +439,39 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
sound_note: { sound_note: {
where: 'device', where: 'device',
default: { type: 'syuilo/n-aec', volume: 1 }, default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
}, },
sound_noteMy: { sound_noteMy: {
where: 'device', where: 'device',
default: { type: 'syuilo/n-cea-4va', volume: 1 }, default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
}, },
sound_noteEdited: { sound_noteEdited: {
where: 'device', where: 'device',
default: { type: 'syuilo/n-eca', volume: 1 }, default: { type: 'syuilo/n-eca', volume: 1 } as SoundStore,
}, },
sound_notification: { sound_notification: {
where: 'device', where: 'device',
default: { type: 'syuilo/n-ea', volume: 1 }, default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
}, },
sound_chat: { sound_chat: {
where: 'device', where: 'device',
default: { type: 'syuilo/pope1', volume: 1 }, default: { type: 'syuilo/pope1', volume: 1 } as SoundStore,
}, },
sound_chatBg: { sound_chatBg: {
where: 'device', where: 'device',
default: { type: 'syuilo/waon', volume: 1 }, default: { type: 'syuilo/waon', volume: 1 } as SoundStore,
}, },
sound_antenna: { sound_antenna: {
where: 'device', where: 'device',
default: { type: 'syuilo/triple', volume: 1 }, default: { type: 'syuilo/triple', volume: 1 } as SoundStore,
}, },
sound_channel: { sound_channel: {
where: 'device', where: 'device',
default: { type: 'syuilo/square-pico', volume: 1 }, default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore,
}, },
sound_reaction: { sound_reaction: {
where: 'device', where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 }, default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
}, },
// #region CherryPick // #region CherryPick

View file

@ -104,7 +104,13 @@ let jammedAudioBuffer: AudioBuffer | null = $ref(null);
let jammedSoundNodePlaying: boolean = $ref(false); let jammedSoundNodePlaying: boolean = $ref(false);
if (defaultStore.state.sound_masterVolume) { if (defaultStore.state.sound_masterVolume) {
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf); sound.loadAudio({
type: 'syuilo/queue-jammed',
volume: 1,
}).then(buf => {
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
jammedAudioBuffer = buf;
});
} }
for (const domain of ['inbox', 'deliver']) { for (const domain of ['inbox', 'deliver']) {