fix(frontend): ドライブの音声が再生できない場合の処理を追加 (#14073)

* fix(frontend): ドライブの音声が再生できない場合の処理を追加

* Update Changelog

* fix lint

* Update packages/frontend/src/scripts/sound.ts

* lint

* Update sound.ts

* fix merge mistakes

* use shorthand operator

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり 2024-07-30 20:30:41 +09:00 committed by GitHub
parent 676c599e48
commit 8b163cd3fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 18 deletions

View File

@ -70,6 +70,7 @@
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正 - Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正 - Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正 - Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正
### Server ### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)

4
locales/index.d.ts vendored
View File

@ -7633,6 +7633,10 @@ export interface Locale extends ILocale {
* 使Misskeyの使用に支障をきたす可能性があります * 使Misskeyの使用に支障をきたす可能性があります
*/ */
"driveFileDurationWarnDescription": string; "driveFileDurationWarnDescription": string;
/**
*
*/
"driveFileError": string;
}; };
"_ago": { "_ago": {
/** /**

View File

@ -2002,6 +2002,7 @@ _soundSettings:
driveFileTypeWarnDescription: "音声ファイルを選択してください" driveFileTypeWarnDescription: "音声ファイルを選択してください"
driveFileDurationWarn: "音声が長すぎます" driveFileDurationWarn: "音声が長すぎます"
driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか" driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか"
driveFileError: "音声が読み込めませんでした。設定を変更してください"
_ago: _ago:
future: "未来" future: "未来"

View File

@ -9,7 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.sound }}</template> <template #label>{{ i18n.ts.sound }}</template>
<option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(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"> <div v-if="type === '_driveFile_' && driveFileError === true" :class="$style.fileSelectorRoot">
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
<div :class="$style.fileErrorRoot">
<MkCondensedLine>{{ i18n.ts._soundSettings.driveFileError }}</MkCondensedLine>
</div>
</div>
<div v-else-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton> <MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
<div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div> <div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
</div> </div>
@ -19,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_buttons"> <div class="_buttons">
<MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</MkButton> <MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</MkButton>
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton inline primary :disabled="!hasChanged || driveFileError" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import type { SoundType } from '@/scripts/sound.js'; 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';
@ -51,13 +57,18 @@ const type = ref<SoundType>(props.type);
const fileId = ref(props.fileId); const fileId = ref(props.fileId);
const fileUrl = ref(props.fileUrl); const fileUrl = ref(props.fileUrl);
const fileName = ref<string>(''); const fileName = ref<string>('');
const driveFileError = ref(false);
const hasChanged = ref(false);
const volume = ref(props.volume); const volume = ref(props.volume);
if (type.value === '_driveFile_' && fileId.value) { if (type.value === '_driveFile_' && fileId.value) {
const apiRes = await misskeyApi('drive/files/show', { await misskeyApi('drive/files/show', {
fileId: fileId.value, fileId: fileId.value,
}).then((res) => {
fileName.value = res.name;
}).catch((res) => {
driveFileError.value = true;
}); });
fileName.value = apiRes.name;
} }
function getSoundTypeName(f: SoundType): string { function getSoundTypeName(f: SoundType): string {
@ -107,9 +118,21 @@ function selectSound(ev) {
fileUrl.value = file.url; fileUrl.value = file.url;
fileName.value = file.name; fileName.value = file.name;
fileId.value = file.id; fileId.value = file.id;
driveFileError.value = false;
hasChanged.value = true;
}); });
} }
watch([type, volume], ([typeTo, volumeTo], [typeFrom, volumeFrom]) => {
if (typeFrom !== typeTo && typeTo !== '_driveFile_') {
fileUrl.value = undefined;
fileName.value = '';
fileId.value = undefined;
driveFileError.value = false;
}
hasChanged.value = true;
});
function listen() { function listen() {
if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) { if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
os.alert({ os.alert({
@ -131,6 +154,10 @@ function listen() {
} }
function save() { function save() {
if (hasChanged.value === false || driveFileError.value === true) {
return;
}
if (type.value === '_driveFile_' && !fileUrl.value) { if (type.value === '_driveFile_' && !fileUrl.value) {
os.alert({ os.alert({
type: 'warning', type: 'warning',
@ -163,6 +190,13 @@ function save() {
gap: 8px; gap: 8px;
} }
.fileErrorRoot {
flex-grow: 1;
min-width: 0;
font-weight: 700;
color: var(--error);
}
.fileSelectorButton { .fileSelectorButton {
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -21,8 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-for="type in operationTypes" :key="type"> <MkFolder v-for="type in operationTypes" :key="type">
<template #label>{{ i18n.ts._sfx[type] }}</template> <template #label>{{ i18n.ts._sfx[type] }}</template>
<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template> <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<Suspense>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/> <template #default>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
</template>
<template #fallback>
<MkLoading/>
</template>
</Suspense>
</MkFolder> </MkFolder>
</div> </div>
</FormSection> </FormSection>

View File

@ -124,23 +124,33 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
*/ */
export function playMisskeySfx(operationType: OperationType) { export function playMisskeySfx(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`]; const sound = defaultStore.state[`sound_${operationType}`];
playMisskeySfxFile(sound); playMisskeySfxFile(sound).then((succeed) => {
if (!succeed && sound.type === '_driveFile_') {
// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
playMisskeySfxFileInternal({
type: soundName,
volume: sound.volume,
});
}
});
} }
/** /**
* *
* @param soundStore * @param soundStore
*/ */
export function playMisskeySfxFile(soundStore: SoundStore) { export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolean> {
// 連続して再生しない // 連続して再生しない
if (!canPlay) return; if (!canPlay) return false;
// ユーザーアクティベーションが必要な場合はそれがない場合は再生しない // ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return; if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false;
// サウンドがない場合は再生しない // サウンドがない場合は再生しない
if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return; if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false;
canPlay = false; canPlay = false;
playMisskeySfxFileInternal(soundStore).finally(() => { return await playMisskeySfxFileInternal(soundStore).finally(() => {
// ごく短時間に音が重複しないように // ごく短時間に音が重複しないように
setTimeout(() => { setTimeout(() => {
canPlay = true; canPlay = true;
@ -148,19 +158,22 @@ export function playMisskeySfxFile(soundStore: SoundStore) {
}); });
} }
async function playMisskeySfxFileInternal(soundStore: SoundStore) { async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boolean> {
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
return; return false;
} }
const masterVolume = defaultStore.state.sound_masterVolume; const masterVolume = defaultStore.state.sound_masterVolume;
if (isMute() || masterVolume === 0 || soundStore.volume === 0) { if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
return; return true; // ミュート時は成功として扱う
} }
const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
const buffer = await loadAudio(url); const buffer = await loadAudio(url).catch(() => {
if (!buffer) return; return undefined;
});
if (!buffer) return false;
const volume = soundStore.volume * masterVolume; const volume = soundStore.volume * masterVolume;
createSourceNode(buffer, { volume }).soundSource.start(); createSourceNode(buffer, { volume }).soundSource.start();
return true;
} }
export async function playUrl(url: string, opts: { export async function playUrl(url: string, opts: {