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',
|
||||
|
||||
@@ -508,3 +508,105 @@ func (a *App) DownloadAndInstallUpdate() error {
|
||||
func (a *App) RestartApp() error {
|
||||
return a.updater.RestartApp()
|
||||
}
|
||||
|
||||
// Sound Management
|
||||
|
||||
func (a *App) getSoundsDir() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
soundsDir := filepath.Join(configDir, "DailyTimer", "sounds")
|
||||
if err := os.MkdirAll(soundsDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return soundsDir, nil
|
||||
}
|
||||
|
||||
func (a *App) SelectCustomSound(soundType string) (string, error) {
|
||||
// Validate sound type
|
||||
validTypes := map[string]bool{"warning": true, "timeup": true, "meeting_end": true}
|
||||
if !validTypes[soundType] {
|
||||
return "", fmt.Errorf("invalid sound type: %s", soundType)
|
||||
}
|
||||
|
||||
// Open file dialog
|
||||
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Select Sound File",
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "Audio Files", Pattern: "*.mp3;*.wav;*.m4a;*.ogg"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if selection == "" {
|
||||
return "", nil // User cancelled
|
||||
}
|
||||
|
||||
// Get sounds directory
|
||||
soundsDir, err := a.getSoundsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Determine destination filename
|
||||
ext := filepath.Ext(selection)
|
||||
destPath := filepath.Join(soundsDir, soundType+ext)
|
||||
|
||||
// Copy file
|
||||
src, err := os.Open(selection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
if _, err := dst.ReadFrom(src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return destPath, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCustomSoundPath(soundType string) string {
|
||||
soundsDir, err := a.getSoundsDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for common audio extensions
|
||||
extensions := []string{".mp3", ".wav", ".m4a", ".ogg"}
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(soundsDir, soundType+ext)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) ClearCustomSound(soundType string) error {
|
||||
soundsDir, err := a.getSoundsDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extensions := []string{".mp3", ".wav", ".m4a", ".ogg"}
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(soundsDir, soundType+ext)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user