Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src-vite/src/common/sound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Short UI feedback sounds, synthesized with the Web Audio API (no audio assets).
*/

export type UiSound = 'next' | 'close';

// frequency (Hz) and duration (s) of each sound
const SOUNDS: Record<UiSound, { frequency: number; duration: number }> = {
next: { frequency: 880, duration: 0.06 },
close: { frequency: 440, duration: 0.09 },
};

const VOLUME = 0.15;

let audioContext: AudioContext | null = null;

export function playUiSound(sound: UiSound) {
try {
audioContext ??= new AudioContext();
if (audioContext.state === 'suspended') {
void audioContext.resume();
}

const { frequency, duration } = SOUNDS[sound];
const now = audioContext.currentTime;
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();

oscillator.type = 'sine';
oscillator.frequency.value = frequency;
// quick attack and exponential decay to avoid clicks
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(VOLUME, now + 0.005);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);

oscillator.connect(gain).connect(audioContext.destination);
oscillator.start(now);
oscillator.stop(now + duration);
} catch (error) {
console.warn('playUiSound failed:', error);
}
}
26 changes: 21 additions & 5 deletions src-vite/src/components/MediaViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
:icon="IconNext"
:disabled="fileIndex < 0 || fileIndex >= fileCount - 1 || isSlideShow || !canInteract"
:tooltip="$t('image_viewer.toolbar.next')"
@click="triggerNext"
@click="clickNext"
/>
<div class="flex items-center gap-0.5">
<TButton
Expand Down Expand Up @@ -213,7 +213,7 @@
:icon="IconClose"
:tooltip="$t('image_viewer.toolbar.close')"
:disabled="!canInteract"
@click.stop="$emit('close')"
@click.stop="clickClose"
/>
</div>
</div>
Expand All @@ -230,7 +230,7 @@
/>
<IconClose
class="p-3 w-12 h-10 text-base-content/70 hover:text-base-content hover:bg-red-500 transition-colors duration-300 cursor-pointer"
@click.stop="$emit('close')"
@click.stop="clickClose"
/>
</div>

Expand All @@ -240,7 +240,7 @@
<button
v-if="mode === 0 && !config.mediaViewer.isPinned && !isFullScreen"
class="absolute right-2 top-2 z-90 p-2 rounded-full text-base-content/70 bg-base-100/30 hover:text-base-content hover:bg-base-100/70 cursor-pointer"
@click.stop="$emit('close')"
@click.stop="clickClose"
@dblclick.stop
>
<IconClose class="w-5 h-5" />
Expand Down Expand Up @@ -313,7 +313,7 @@
isHoverRight ? (hasNext ? 'opacity-100 pointer-events-auto hover:text-base-content hover:bg-base-100/80 cursor-pointer' : 'opacity-30 cursor-default') : 'opacity-0 pointer-events-none'
]"
:disabled="!hasNext"
@click.stop="triggerNext"
@click.stop="clickNext"
@dblclick.stop
>
<IconRight class="w-8 h-8" />
Expand Down Expand Up @@ -452,6 +452,7 @@ import { isWin, isMac, isLinux, getSlideShowInterval } from '@/common/utils';
import { getShortcutLabel, ShortcutActionId, ShortcutPlatform, VIEW_BACKGROUND_SHORTCUTS } from '@/common/shortcuts';
import { isOriginalUnavailable, setFileAccessibility } from '@/common/availability';
import { getMotionPhotoVideoPath, checkFileAccessibility } from '@/common/api';
import { playUiSound } from '@/common/sound';

import Image from '@/components/Image.vue';
import TButton from '@/components/TButton.vue';
Expand Down Expand Up @@ -1251,6 +1252,21 @@ const triggerNext = () => {
}
}

// Next/Close button clicks (not keyboard or wheel) play a sound when enabled in settings
const clickNext = () => {
if (props.hasNext && config.settings.viewerButtonSound) {
playUiSound('next');
}
triggerNext();
}

const clickClose = () => {
if (config.settings.viewerButtonSound) {
playUiSound('close');
}
emit('close');
}

const handleToggleSlideShow = () => {
if (!props.isSlideShow) {
emit('update:isZoomFit', true);
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Video",
"auto_play_video": "Video Autoplay",
"loop_video": "Video wiederholen"
"loop_video": "Video wiederholen",
"button_sound": "Schaltflächenton",
"button_sound_hint": "Beim Klicken auf „Nächstes“ oder „Schließen“ einen Ton abspielen"
},
"image_search": {
"title": "Suche",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Video",
"auto_play_video": "Auto play video",
"loop_video": "Loop video"
"loop_video": "Loop video",
"button_sound": "Button sound",
"button_sound_hint": "Play a sound when clicking the Next or Close button"
},
"image_search": {
"title": "Search",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Vídeo",
"auto_play_video": "Autoreproducir vídeo",
"loop_video": "Repetir vídeo"
"loop_video": "Repetir vídeo",
"button_sound": "Sonido de botones",
"button_sound_hint": "Reproducir un sonido al hacer clic en Siguiente o Cerrar"
},
"image_search": {
"title": "Buscar",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Vidéo",
"auto_play_video": "Lecture auto vidéo",
"loop_video": "Vidéo en boucle"
"loop_video": "Vidéo en boucle",
"button_sound": "Son des boutons",
"button_sound_hint": "Jouer un son lors d’un clic sur Suivant ou Fermer"
},
"image_search": {
"title": "Recherche",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Videó",
"auto_play_video": "Videó automatikus lejátszása",
"loop_video": "Videó ismétlése"
"loop_video": "Videó ismétlése",
"button_sound": "Gombhang",
"button_sound_hint": "Hang lejátszása a Következő vagy a Bezárás gomb megnyomásakor"
},
"image_search": {
"title": "Keresés",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Video",
"auto_play_video": "Riproduci video automaticamente",
"loop_video": "Ripeti video"
"loop_video": "Ripeti video",
"button_sound": "Suono dei pulsanti",
"button_sound_hint": "Riproduci un suono quando fai clic su Successiva o Chiudi"
},
"image_search": {
"title": "Ricerca",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "ビデオ",
"auto_play_video": "動画自動再生",
"loop_video": "動画を繰り返す"
"loop_video": "動画を繰り返す",
"button_sound": "ボタンのサウンド",
"button_sound_hint": "「次へ」または「閉じる」ボタンをクリックしたときにサウンドを再生"
},
"image_search": {
"title": "検索",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "비디오",
"auto_play_video": "비디오 자동 재생",
"loop_video": "비디오 반복 재생"
"loop_video": "비디오 반복 재생",
"button_sound": "버튼 소리",
"button_sound_hint": "다음 또는 닫기 버튼을 클릭할 때 소리 재생"
},
"image_search": {
"title": "검색",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Video",
"auto_play_video": "Video automatisch afspelen",
"loop_video": "Video herhalen"
"loop_video": "Video herhalen",
"button_sound": "Knopgeluid",
"button_sound_hint": "Speel een geluid af bij klikken op Volgende of Sluiten"
},
"image_search": {
"title": "Zoeken",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Wideo",
"auto_play_video": "Automatycznie odtwarzaj filmy",
"loop_video": "Zapętlaj filmy"
"loop_video": "Zapętlaj filmy",
"button_sound": "Dźwięk przycisków",
"button_sound_hint": "Odtwarzaj dźwięk po kliknięciu przycisku Następne lub Zamknij"
},
"image_search": {
"title": "Wyszukiwanie",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Vídeo",
"auto_play_video": "Auto reproduzir vídeo",
"loop_video": "Repetir vídeo"
"loop_video": "Repetir vídeo",
"button_sound": "Som dos botões",
"button_sound_hint": "Reproduzir um som ao clicar em Próximo ou Fechar"
},
"image_search": {
"title": "Pesquisa",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Видео",
"auto_play_video": "Автовоспроизведение видео",
"loop_video": "Зациклить видео"
"loop_video": "Зациклить видео",
"button_sound": "Звук кнопок",
"button_sound_hint": "Воспроизводить звук при нажатии кнопок «След.» и «Закрыть»"
},
"image_search": {
"title": "Поиск",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "Відео",
"auto_play_video": "Автоматично відтворювати відео",
"loop_video": "Повторювати відео"
"loop_video": "Повторювати відео",
"button_sound": "Звук кнопок",
"button_sound_hint": "Відтворювати звук під час натискання кнопок «Наступна» та «Закрити»"
},
"image_search": {
"title": "Пошук",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "视频",
"auto_play_video": "自动播放视频",
"loop_video": "循环播放视频"
"loop_video": "循环播放视频",
"button_sound": "按钮提示音",
"button_sound_hint": "点击“下一张”或“关闭”按钮时播放提示音"
},
"image_search": {
"title": "搜索",
Expand Down
4 changes: 3 additions & 1 deletion src-vite/src/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,9 @@
],
"section_video": "影片",
"auto_play_video": "自動播放影片",
"loop_video": "循環播放影片"
"loop_video": "循環播放影片",
"button_sound": "按鈕提示音",
"button_sound_hint": "點擊「下一張」或「關閉」按鈕時播放提示音"
},
"image_search": {
"title": "搜尋",
Expand Down
3 changes: 3 additions & 0 deletions src-vite/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ if (isMainWindow) {
listen('settings-loopVideo-changed', (event) => {
config.settings.loopVideo = event.payload
})
listen('settings-viewerButtonSound-changed', (event) => {
config.settings.viewerButtonSound = event.payload
})
listen('settings-groupRawJpegPairs-changed', (event) => {
config.settings.groupRawJpegPairs = event.payload
})
Expand Down
1 change: 1 addition & 0 deletions src-vite/src/stores/configStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const useConfigStore = defineStore('configStore', {
viewBackground: 0, // 0: default, 1: black, 2: dark gray, 3: medium gray, 4: light gray, 5: white
autoPlayVideo: true, // auto play video
loopVideo: false, // loop video (only effective when autoPlayVideo is off)
viewerButtonSound: false, // play a sound when clicking the Next / Close buttons
// showComment: false, // show comment
externalApps: {
image: { defaultId: null, apps: [] },
Expand Down
10 changes: 10 additions & 0 deletions src-vite/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@
</span>
<input type="checkbox" class="toggle toggle-primary toggle-sm" v-model="config.settings.navigatorSharpnessGrid" />
</label>
<label class="flex items-center justify-between px-1 rounded-box hover:bg-base-100/10 transition-colors duration-200">
<span class="flex flex-col gap-0.5 text-sm leading-5">
<span>{{ $t('settings.image_view.button_sound') }}</span>
<span class="text-xs text-base-content/30">{{ $t('settings.image_view.button_sound_hint') }}</span>
</span>
<input type="checkbox" class="toggle toggle-primary toggle-sm" v-model="config.settings.viewerButtonSound" />
</label>
</div>

<!-- view -->
Expand Down Expand Up @@ -1514,6 +1521,9 @@ watch(() => config.settings.autoPlayVideo, (newValue) => {
watch(() => config.settings.loopVideo, (newValue) => {
emit('settings-loopVideo-changed', newValue);
});
watch(() => config.settings.viewerButtonSound, (newValue) => {
emit('settings-viewerButtonSound-changed', newValue);
});

// image search settings
watch(() => config.settings.imageSearch.model, (newValue) => {
Expand Down