feat: add custom sound upload and fix localization
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -94,6 +94,11 @@ export const translations = {
|
||||
sound: 'Звуковые уведомления',
|
||||
soundEnabled: 'Включены',
|
||||
soundDisabled: 'Выключены',
|
||||
testWarning: 'Предупреждение',
|
||||
testTimeUp: 'Время вышло',
|
||||
testMeetingEnd: 'Конец собрания',
|
||||
customSound: 'свой',
|
||||
defaultSound: 'стандартный',
|
||||
warningTime: 'Предупреждение за',
|
||||
seconds: 'сек',
|
||||
defaultSpeakerTime: 'Время на спикера по умолчанию',
|
||||
@@ -262,6 +267,11 @@ export const translations = {
|
||||
sound: 'Sound Notifications',
|
||||
soundEnabled: 'Enabled',
|
||||
soundDisabled: 'Disabled',
|
||||
testWarning: 'Warning',
|
||||
testTimeUp: 'Time Up',
|
||||
testMeetingEnd: 'Meeting End',
|
||||
customSound: 'custom',
|
||||
defaultSound: 'default',
|
||||
warningTime: 'Warning before',
|
||||
seconds: 'sec',
|
||||
defaultSpeakerTime: 'Default Speaker Time',
|
||||
|
||||
Reference in New Issue
Block a user