Skip to content

Commit a3b5f6d

Browse files
committed
feat: enhance settings panel with unsaved changes confirmation and add open in explorer functionality
1 parent eacddc1 commit a3b5f6d

File tree

9 files changed

+126
-9
lines changed

9 files changed

+126
-9
lines changed

messages/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@
4141

4242
"settings_storage_title": "Storage",
4343
"settings_storage_clipDir": "Clip Output Directory",
44+
"settings_storage_openClipDir": "Open clip directory",
4445
"settings_storage_dbLocation": "Database Location",
4546

4647
"settings_saved": "Saved",
48+
"settings_unsavedModal_title": "Unsaved Changes",
49+
"settings_unsavedModal_warning": "You have unsaved settings changes. If you leave now, your changes will be lost.",
50+
"settings_unsavedModal_discard": "Discard Changes",
51+
"settings_unsavedModal_stayOnPage": "Stay on Page",
4752

4853
"settings_models_installed": "Installed",
4954
"settings_models_catalog": "Catalog",
@@ -192,6 +197,8 @@
192197
"analysis_openFolder": "Open Folder",
193198
"analysis_model": "Model",
194199
"analysis_locationDate": "Location & Date",
200+
"analysis_statusCoords": "Coords",
201+
"analysis_statusDate": "Date",
195202
"analysis_previousLocation": "Previous location...",
196203
"analysis_recordingDatePlaceholder": "Recording date...",
197204
"analysis_recordingDate": "Recording Date",

src/main/ipc/settings.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcMain, dialog, app } from 'electron';
1+
import { ipcMain, dialog, shell, app } from 'electron';
22
import fs from 'fs';
33
import path from 'path';
44
import { getConfig, getConfigPath } from '../birda/config';
@@ -116,13 +116,18 @@ export async function registerSettingsHandlers(): Promise<void> {
116116
return result.canceled ? null : result.filePaths[0];
117117
});
118118

119-
ipcMain.handle('fs:open-folder-dialog', async (_event) => {
119+
ipcMain.handle('fs:open-folder-dialog', async (_event, defaultPath?: string) => {
120120
const result = await dialog.showOpenDialog({
121121
properties: ['openDirectory'],
122+
...(defaultPath ? { defaultPath } : {}),
122123
});
123124
return result.canceled ? null : result.filePaths[0];
124125
});
125126

127+
ipcMain.handle('fs:open-in-explorer', async (_event, folderPath: string) => {
128+
await shell.openPath(folderPath);
129+
});
130+
126131
ipcMain.handle('fs:read-coordinates', async (_event, folderPath: string) => {
127132
const coordFile = path.join(folderPath, 'coordinates.txt');
128133
try {

src/preload/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const ALLOWED_INVOKE_CHANNELS = new Set([
3434
'fs:open-folder-dialog',
3535
'fs:read-coordinates',
3636
'fs:scan-source',
37+
'fs:open-in-explorer',
3738
]);
3839

3940
const ALLOWED_RECEIVE_CHANNELS = new Set([

src/renderer/src/app.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@
9191
--noise: 0;
9292
}
9393

94+
/* Opt in to stylable select popups (Chromium 134+) */
95+
.select {
96+
appearance: base-select;
97+
}
98+
99+
.select::picker(select) {
100+
border: 1px solid var(--color-base-300);
101+
border-radius: var(--radius-box);
102+
box-shadow:
103+
0 4px 6px -1px rgb(0 0 0 / 0.1),
104+
0 2px 4px -2px rgb(0 0 0 / 0.1);
105+
}
106+
94107
/* Override daisyUI double-border focus style with a clean single highlight */
95108
.input:focus,
96109
.input:focus-within,

src/renderer/src/lib/components/SettingsPanel.svelte

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
getBirdaConfig,
2525
openExecutableDialog,
2626
openFolderDialog,
27+
openInExplorer,
2728
getAvailableLanguages,
2829
getCatalogStats,
2930
clearDatabase,
@@ -96,6 +97,7 @@
9697
let birdaStatus = $state<{ available: boolean; path?: string; error?: string } | null>(null);
9798
let birdaConfig = $state<Record<string, unknown> | null>(null);
9899
let availableLanguages = $state<{ code: string; name: string }[]>([]);
100+
let savedSettings = $state<AppSettings | null>(null);
99101
let saving = $state(false);
100102
let saved = $state(false);
101103
let error = $state<string | null>(null);
@@ -143,10 +145,19 @@
143145
appState.theme = settings.theme;
144146
});
145147
148+
$effect(() => {
149+
if (!savedSettings) {
150+
appState.settingsHasUnsavedChanges = false;
151+
return;
152+
}
153+
appState.settingsHasUnsavedChanges = JSON.stringify($state.snapshot(settings)) !== JSON.stringify(savedSettings);
154+
});
155+
146156
async function load() {
147157
try {
148158
const loaded = await getSettings();
149159
settings = { ...settings, ...loaded };
160+
savedSettings = structuredClone($state.snapshot(settings));
150161
appState.theme = loaded.theme;
151162
152163
birdaStatus = await checkBirda();
@@ -200,6 +211,7 @@
200211
error = null;
201212
try {
202213
settings = await setSettings($state.snapshot(settings));
214+
savedSettings = structuredClone($state.snapshot(settings));
203215
} catch (e) {
204216
error = (e as Error).message;
205217
} finally {
@@ -214,6 +226,7 @@
214226
try {
215227
const previousLang = settings.ui_language;
216228
settings = await setSettings($state.snapshot(settings));
229+
savedSettings = structuredClone($state.snapshot(settings));
217230
birdaStatus = await checkBirda();
218231
219232
// If UI language changed, reload to apply new locale
@@ -238,7 +251,7 @@
238251
}
239252
240253
async function browseClipDir() {
241-
const path = await openFolderDialog();
254+
const path = await openFolderDialog(settings.clip_output_dir || undefined);
242255
if (path) settings.clip_output_dir = path;
243256
}
244257
@@ -263,6 +276,7 @@
263276
onDestroy(() => {
264277
if (savedTimer) clearTimeout(savedTimer);
265278
if (clearResultTimer) clearTimeout(clearResultTimer);
279+
appState.settingsHasUnsavedChanges = false;
266280
});
267281
</script>
268282

@@ -437,6 +451,14 @@
437451
<span class="text-base-content/70 text-sm font-medium">{m.settings_storage_clipDir()}</span>
438452
<div class="mt-1 flex gap-2">
439453
<input type="text" bind:value={settings.clip_output_dir} class="input input-bordered flex-1" />
454+
<button
455+
onclick={() => settings.clip_output_dir && openInExplorer(settings.clip_output_dir)}
456+
disabled={!settings.clip_output_dir}
457+
class="btn btn-outline gap-1.5"
458+
title={m.settings_storage_openClipDir()}
459+
>
460+
<ExternalLink size={16} />
461+
</button>
440462
<button onclick={browseClipDir} class="btn btn-outline gap-1.5">
441463
<FolderOpen size={16} />
442464
{m.common_button_browse()}

src/renderer/src/lib/components/Sidebar.svelte

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { AudioLines, List, Map, Settings } from '@lucide/svelte';
2+
import { AudioLines, List, Map, Settings, TriangleAlert } from '@lucide/svelte';
33
import { appState, type Tab } from '$lib/stores/app.svelte';
44
import * as m from '$paraglide/messages';
55
@@ -9,13 +9,37 @@
99
{ id: 'map', label: m.sidebar_map(), icon: Map },
1010
{ id: 'settings', label: m.sidebar_settings(), icon: Settings },
1111
];
12+
13+
let pendingTab = $state<Tab | null>(null);
14+
15+
function handleTabClick(tabId: Tab) {
16+
if (tabId === appState.activeTab) return;
17+
if (appState.activeTab === 'settings' && appState.settingsHasUnsavedChanges) {
18+
pendingTab = tabId;
19+
return;
20+
}
21+
appState.activeTab = tabId;
22+
}
23+
24+
function confirmDiscard() {
25+
if (pendingTab) {
26+
appState.activeTab = pendingTab;
27+
pendingTab = null;
28+
}
29+
}
30+
31+
function cancelDiscard() {
32+
pendingTab = null;
33+
}
1234
</script>
1335

1436
<nav class="border-base-300 bg-base-200 flex w-[72px] shrink-0 flex-col border-r">
1537
<div class="flex flex-col gap-1 py-2">
1638
{#each tabs as tab (tab.id)}
1739
<button
18-
onclick={() => (appState.activeTab = tab.id)}
40+
onclick={() => {
41+
handleTabClick(tab.id);
42+
}}
1943
class="relative mx-auto flex w-full flex-col items-center gap-0.5 px-1 py-2.5 transition-colors
2044
{appState.activeTab === tab.id
2145
? 'text-primary'
@@ -32,3 +56,29 @@
3256
{/each}
3357
</div>
3458
</nav>
59+
60+
<!-- Unsaved Settings Confirmation Modal -->
61+
{#if pendingTab}
62+
<dialog class="modal modal-open">
63+
<div class="modal-box">
64+
<div class="text-warning flex items-center gap-3">
65+
<TriangleAlert size={24} />
66+
<h3 class="text-lg font-semibold">{m.settings_unsavedModal_title()}</h3>
67+
</div>
68+
<p class="text-base-content/70 mt-3 text-sm">
69+
{m.settings_unsavedModal_warning()}
70+
</p>
71+
<div class="modal-action">
72+
<button onclick={confirmDiscard} class="btn btn-warning">
73+
{m.settings_unsavedModal_discard()}
74+
</button>
75+
<button onclick={cancelDiscard} class="btn btn-primary">
76+
{m.settings_unsavedModal_stayOnPage()}
77+
</button>
78+
</div>
79+
</div>
80+
<form method="dialog" class="modal-backdrop">
81+
<button onclick={cancelDiscard}>close</button>
82+
</form>
83+
</dialog>
84+
{/if}

src/renderer/src/lib/stores/app.svelte.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export const appState = $state({
2020
lastRunId: null as number | null,
2121
lastSourceFile: null as string | null,
2222
theme: 'system' as 'system' | 'light' | 'dark',
23+
settingsHasUnsavedChanges: false,
2324
});

src/renderer/src/lib/utils/ipc.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,12 @@ export function openExecutableDialog(): Promise<string | null> {
122122
return window.birda.invoke('fs:open-executable-dialog') as Promise<string | null>;
123123
}
124124

125-
export function openFolderDialog(): Promise<string | null> {
126-
return window.birda.invoke('fs:open-folder-dialog') as Promise<string | null>;
125+
export function openFolderDialog(defaultPath?: string): Promise<string | null> {
126+
return window.birda.invoke('fs:open-folder-dialog', defaultPath) as Promise<string | null>;
127+
}
128+
129+
export function openInExplorer(folderPath: string): Promise<void> {
130+
return window.birda.invoke('fs:open-in-explorer', folderPath) as Promise<void>;
127131
}
128132

129133
export function scanSource(sourcePath: string): Promise<SourceScanResult> {

src/renderer/src/pages/AnalysisPage.svelte

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
ChevronLeft,
1212
ChevronRight,
1313
TriangleAlert,
14+
Check,
15+
Minus,
1416
} from '@lucide/svelte';
1517
import AnalysisTable from '$lib/components/AnalysisTable.svelte';
1618
import CoordinateInput from '$lib/components/CoordinateInput.svelte';
@@ -499,7 +501,19 @@
499501

500502
<!-- Location & Date section -->
501503
<div class="border-base-300 space-y-3 rounded-lg border p-3">
502-
<h3 class="text-base-content/50 text-xs font-medium">{m.analysis_locationDate()}</h3>
504+
<div class="flex items-center gap-1.5">
505+
<h3 class="text-base-content/50 text-xs font-medium">{m.analysis_locationDate()}</h3>
506+
<div class="ml-auto flex gap-1">
507+
<span class="badge badge-xs gap-0.5 {hasCoords ? 'badge-success' : 'badge-ghost text-base-content/30'}">
508+
{#if hasCoords}<Check size={10} />{:else}<Minus size={10} />{/if}
509+
{m.analysis_statusCoords()}
510+
</span>
511+
<span class="badge badge-xs gap-0.5 {hasDate ? 'badge-success' : 'badge-ghost text-base-content/30'}">
512+
{#if hasDate}<Check size={10} />{:else}<Minus size={10} />{/if}
513+
{m.analysis_statusDate()}
514+
</span>
515+
</div>
516+
</div>
503517

504518
<!-- Previous locations dropdown -->
505519
{#if previousLocations.length > 0}
@@ -581,7 +595,7 @@
581595
{:else}
582596
<button
583597
onclick={handleStartClick}
584-
class="btn btn-primary shadow-primary/25 hover:shadow-primary/30 w-full gap-2 shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110"
598+
class="btn btn-primary w-full gap-2 transition-all duration-200 hover:brightness-110"
585599
>
586600
<Play size={18} />
587601
{m.analysis_startAnalysis()}

0 commit comments

Comments
 (0)