feat: add custom sound upload and fix localization

This commit is contained in:
Mikhail Kiselev
2026-02-10 16:19:39 +03:00
parent 30af8729b8
commit 809f64b93d
3 changed files with 319 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
<script>
import { onMount, createEventDispatcher } from 'svelte'
import { GetSettings, UpdateSettings, GetMeeting, UpdateMeeting, GetVersion, CheckForUpdates, DownloadAndInstallUpdate, RestartApp } from '../../wailsjs/go/app/App'
import { GetSettings, UpdateSettings, GetMeeting, UpdateMeeting, GetVersion, CheckForUpdates, DownloadAndInstallUpdate, RestartApp, SelectCustomSound, GetCustomSoundPath, ClearCustomSound } from '../../wailsjs/go/app/App'
import { WindowSetSize, ScreenGetAll, EventsOn, EventsOff } from '../../wailsjs/runtime/runtime'
import { t, locale, setLocale } from '../lib/i18n'
@@ -16,6 +16,14 @@
let windowFullHeight = true
let audioContext = null
// Custom sounds state
let customSounds = {
warning: null,
timeup: null,
meeting_end: null
}
let audioElements = {}
// Update state
let currentVersion = 'dev'
let updateInfo = null
@@ -35,6 +43,10 @@
function playBeep(frequency, duration, type = 'sine') {
try {
const ctx = getAudioContext()
// Resume context if suspended (required by browsers on first interaction)
if (ctx.state === 'suspended') {
ctx.resume()
}
const oscillator = ctx.createOscillator()
const gainNode = ctx.createGain()
@@ -56,6 +68,13 @@
}
function testSound(name) {
// If custom sound exists, play it
if (customSounds[name]) {
playCustomSound(name)
return
}
// Otherwise play default beep sounds
switch (name) {
case 'warning':
playBeep(880, 0.15)
@@ -74,8 +93,63 @@
}
}
function playCustomSound(name) {
try {
if (!audioElements[name]) {
audioElements[name] = new Audio('file://' + customSounds[name])
}
audioElements[name].currentTime = 0
audioElements[name].play()
} catch (e) {
console.error('Failed to play custom sound:', e)
}
}
async function loadCustomSounds() {
const types = ['warning', 'timeup', 'meeting_end']
for (const type of types) {
try {
const path = await GetCustomSoundPath(type)
if (path) {
customSounds[type] = path
// Pre-create audio element
audioElements[type] = new Audio('file://' + path)
}
} catch (e) {
console.error(`Failed to get custom sound for ${type}:`, e)
}
}
customSounds = { ...customSounds } // Trigger reactivity
}
async function handleUploadSound(soundType) {
try {
const path = await SelectCustomSound(soundType)
if (path) {
customSounds[soundType] = path
// Recreate audio element with new file
audioElements[soundType] = new Audio('file://' + path)
customSounds = { ...customSounds }
}
} catch (e) {
console.error('Failed to upload sound:', e)
}
}
async function handleClearSound(soundType) {
try {
await ClearCustomSound(soundType)
customSounds[soundType] = null
delete audioElements[soundType]
customSounds = { ...customSounds }
} catch (e) {
console.error('Failed to clear sound:', e)
}
}
onMount(async () => {
await loadData()
await loadCustomSounds()
// Load version and check for updates
try {
@@ -247,10 +321,60 @@
<label for="soundEnabled">{settings.soundEnabled ? $t('settings.soundEnabled') : $t('settings.soundDisabled')}</label>
</div>
<div class="sound-test-buttons">
<button type="button" class="test-btn" on:click={() => testSound('warning')}>🔔 Test Warning</button>
<button type="button" class="test-btn" on:click={() => testSound('timeup')}> Test Time Up</button>
<button type="button" class="test-btn" on:click={() => testSound('meeting_end')}>🏁 Test Meeting End</button>
<div class="sound-items">
<div class="sound-item">
<div class="sound-info">
<span class="sound-label">🔔 {$t('settings.testWarning')}</span>
{#if customSounds.warning}
<span class="sound-status custom">{$t('settings.customSound')}</span>
{:else}
<span class="sound-status default">{$t('settings.defaultSound')}</span>
{/if}
</div>
<div class="sound-actions">
<button type="button" class="test-btn" on:click={() => testSound('warning')}>▶</button>
<button type="button" class="upload-btn" on:click={() => handleUploadSound('warning')}>📁</button>
{#if customSounds.warning}
<button type="button" class="clear-btn" on:click={() => handleClearSound('warning')}>✕</button>
{/if}
</div>
</div>
<div class="sound-item">
<div class="sound-info">
<span class="sound-label">{$t('settings.testTimeUp')}</span>
{#if customSounds.timeup}
<span class="sound-status custom">{$t('settings.customSound')}</span>
{:else}
<span class="sound-status default">{$t('settings.defaultSound')}</span>
{/if}
</div>
<div class="sound-actions">
<button type="button" class="test-btn" on:click={() => testSound('timeup')}>▶</button>
<button type="button" class="upload-btn" on:click={() => handleUploadSound('timeup')}>📁</button>
{#if customSounds.timeup}
<button type="button" class="clear-btn" on:click={() => handleClearSound('timeup')}>✕</button>
{/if}
</div>
</div>
<div class="sound-item">
<div class="sound-info">
<span class="sound-label">🏁 {$t('settings.testMeetingEnd')}</span>
{#if customSounds.meeting_end}
<span class="sound-status custom">{$t('settings.customSound')}</span>
{:else}
<span class="sound-status default">{$t('settings.defaultSound')}</span>
{/if}
</div>
<div class="sound-actions">
<button type="button" class="test-btn" on:click={() => testSound('meeting_end')}>▶</button>
<button type="button" class="upload-btn" on:click={() => handleUploadSound('meeting_end')}>📁</button>
{#if customSounds.meeting_end}
<button type="button" class="clear-btn" on:click={() => handleClearSound('meeting_end')}>✕</button>
{/if}
</div>
</div>
</div>
</section>
@@ -498,6 +622,84 @@
transform: scale(0.97);
}
/* Sound items */
.sound-items {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.sound-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #1b2636;
border: 1px solid #3d4f61;
border-radius: 8px;
}
.sound-info {
display: flex;
align-items: center;
gap: 10px;
}
.sound-label {
font-size: 14px;
color: #e0e0e0;
}
.sound-status {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
}
.sound-status.custom {
background: #2a4a3a;
color: #6ee7b7;
}
.sound-status.default {
background: #3d4f61;
color: #9ca3af;
}
.sound-actions {
display: flex;
gap: 6px;
}
.sound-actions button {
width: 32px;
height: 32px;
padding: 0;
border: 1px solid #3d4f61;
border-radius: 6px;
background: #2a3a4e;
color: #9ca3af;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.sound-actions button:hover {
border-color: #4a90d9;
background: #3a4a5e;
color: #e0e0e0;
}
.sound-actions .clear-btn:hover {
border-color: #ef4444;
background: #4a2a2a;
color: #fca5a5;
}
/* Updates section */
.updates-section {
border: 1px solid #3d4f61;