Skip to content
Merged
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
51 changes: 49 additions & 2 deletions src-tauri/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ pub struct MenuTranslations {
pub about: String,
pub settings: String,
pub quit: String,
// App menu items (macOS only: Hide, Hide Others, Show All)
#[serde(default)]
pub hide: String,
#[serde(default)]
pub hide_others: String,
#[serde(default)]
pub show_all: String,
// File menu items
pub import_tracks: String,
pub add_release: String,
Expand Down Expand Up @@ -202,7 +209,7 @@ pub fn build_menu(app: &AppHandle<Wry>) -> Result<Menu<Wry>, tauri::Error> {

fn build_app_menu(app: &AppHandle<Wry>) -> Result<Submenu<Wry>, tauri::Error> {
let app_name = get_app_name();
SubmenuBuilder::with_id(app, ids::APP_MENU, &app_name)
let mut builder = SubmenuBuilder::with_id(app, ids::APP_MENU, &app_name)
.item(&MenuItem::with_id(
app,
ids::ABOUT,
Expand All @@ -218,7 +225,21 @@ fn build_app_menu(app: &AppHandle<Wry>) -> Result<Submenu<Wry>, tauri::Error> {
true,
Some("CmdOrCtrl+,"),
)?)
.separator()
.separator();

#[cfg(target_os = "macos")]
{
builder = builder
.item(&PredefinedMenuItem::hide(
app,
Some(&format!("Hide {app_name}")),
)?)
.item(&PredefinedMenuItem::hide_others(app, Some("Hide Others"))?)
.item(&PredefinedMenuItem::show_all(app, Some("Show All"))?)
.separator();
}

builder
.item(&MenuItem::with_id(
app,
ids::QUIT,
Expand Down Expand Up @@ -708,6 +729,32 @@ pub fn update_menu_translations(
update_item_text(&menu, ids::SETTINGS, &translations.settings)?;
update_item_text(&menu, ids::QUIT, &translations.quit)?;

// Update App menu PredefinedMenuItems (Hide, Hide Others, Show All) — macOS only
#[cfg(target_os = "macos")]
if let Some(MenuItemKind::Submenu(app_submenu)) = menu.get(ids::APP_MENU) {
let app_texts: [&str; 3] = [
&translations.hide,
&translations.hide_others,
&translations.show_all,
];
let predefined: Vec<_> = app_submenu
.items()?
.into_iter()
.filter_map(|item| match item {
MenuItemKind::Predefined(p) => {
if p.text().unwrap_or_default().is_empty() {
return None;
}
Some(p)
}
_ => None,
})
.collect();
for (item, text) in predefined.iter().zip(app_texts.iter()) {
item.set_text(*text)?;
}
}

// Update File menu items
update_item_text(&menu, ids::IMPORT_TRACKS, &translations.import_tracks)?;
update_item_text(&menu, ids::ADD_RELEASE, &translations.add_release)?;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export interface MenuTranslations {
about: string
settings: string
quit: string
// App menu items (macOS only: Hide, Hide Others, Show All)
hide: string
hideOthers: string
showAll: string
// File menu items
importTracks: string
addRelease: string
Expand Down
13 changes: 8 additions & 5 deletions src/lib/components/common/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
type Props = {
open: boolean
title?: string
size?: 'sm' | 'md' | 'lg'
size?: 'sm' | 'md' | 'lg' | 'xl'
flush?: boolean
onClose: () => void
onSubmit?: () => void
children: Snippet
footer?: Snippet
}

let { open, title, size = 'sm', onClose, onSubmit, children, footer }: Props = $props()
let { open, title, size = 'sm', flush = false, onClose, onSubmit, children, footer }: Props = $props()

const sizeClasses: Record<string, string> = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-xl',
xl: 'max-w-2xl',
}

let dialogEl: HTMLDialogElement | undefined = $state()
Expand Down Expand Up @@ -94,14 +96,15 @@

<dialog
bind:this={dialogEl}
class="fixed inset-0 m-0 flex h-full max-h-none w-full max-w-none items-center justify-center bg-transparent p-0 backdrop:bg-black/60"
class="fixed inset-0 m-0 h-full max-h-none w-full max-w-none bg-transparent p-0 backdrop:bg-black/60"
onkeydown={handleKeydown}
onmousedown={handleBackdropMousedown}
onclick={handleBackdropClick}
>
{#if visible}
<div
class="flex max-h-[85vh] w-full {sizeClasses[size] ?? 'max-w-md'} flex-col rounded-lg border border-stroke bg-surface-1 text-text-primary shadow-xl"
class="fixed top-1/2 left-1/2 flex max-h-[85vh] w-full {sizeClasses[size] ??
'max-w-md'} -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-stroke bg-surface-1 text-text-primary shadow-xl"
transition:scale={{ start: 0.95, duration: 200 }}
onoutroend={handleOutroEnd}
>
Expand All @@ -111,7 +114,7 @@
</div>
{/if}

<div class="min-h-0 overflow-y-auto px-4 py-4">
<div class="min-h-0 {flush ? '' : 'overflow-y-auto px-4 py-4'}">
{@render children()}
</div>

Expand Down
216 changes: 55 additions & 161 deletions src/lib/components/settings/SettingsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { settingsStore } from '$lib/stores/settings'
import { diagnosticsStore } from '$lib/stores/diagnostics'
import { Button, Text } from '$lib/components/common'
import Modal from '$lib/components/common/Modal.svelte'
import Icon from '$lib/components/common/Icon.svelte'
import { scale } from 'svelte/transition'
import { translate } from '$lib/i18n'
import { GeneralTab, LibraryTab, DiscoveryTab, AppearanceTab, SoundTab, DiagnosticsTab, AboutTab } from './tabs'

Expand All @@ -16,11 +16,18 @@

let { open, initialTab, onClose }: Props = $props()

let dialogEl: HTMLDialogElement | undefined = $state()
let contentEl: HTMLDivElement | undefined = $state()
let activePage: SettingsPage = $state('general')
let visible = $state(false)
let mousedownTarget: EventTarget | null = $state(null)

const tabs: { page: SettingsPage; icon: string; fill?: boolean }[] = [
{ page: 'general', icon: 'sliders-horizontal' },
{ page: 'appearance', icon: 'palette' },
{ page: 'discovery', icon: 'globe' },
{ page: 'library', icon: 'library' },
{ page: 'sound', icon: 'volume-full', fill: true },
{ page: 'diagnostics', icon: 'terminal' },
{ page: 'about', icon: 'info' },
]

// Set active page when opening (use initialTab if provided, otherwise default to 'general')
$effect(() => {
Expand All @@ -29,21 +36,6 @@
}
})

// Open dialog when open becomes true
$effect(() => {
if (!dialogEl) return
if (open) {
visible = true
dialogEl.showModal()
}
})

// Handle transition end to close dialog
function handleOutroEnd() {
dialogEl?.close()
visible = false
}

// Refresh audio devices when opening sound settings
$effect(() => {
if (open && activePage === 'sound') {
Expand All @@ -64,149 +56,51 @@
activePage
contentEl?.scrollTo(0, 0)
})

function handleKeydown(e: KeyboardEvent) {
e.stopPropagation()
if (e.key === 'Escape') {
e.preventDefault()
onClose()
}
}

function handleBackdropMousedown(e: MouseEvent) {
mousedownTarget = e.target
}

function handleBackdropClick(e: MouseEvent) {
if (e.target === dialogEl && mousedownTarget === dialogEl) {
onClose()
}
}
</script>

<dialog
bind:this={dialogEl}
class="fixed inset-0 m-0 h-full max-h-none w-full max-w-none bg-transparent p-0 backdrop:bg-black/60"
onkeydown={handleKeydown}
onmousedown={handleBackdropMousedown}
onclick={handleBackdropClick}
>
{#if visible}
<div
class="fixed top-1/2 left-1/2 max-h-[80vh] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2
overflow-hidden rounded-lg border border-stroke bg-surface-1 text-text-primary shadow-xl"
transition:scale={{ start: 0.95, duration: 200 }}
onoutroend={handleOutroEnd}
>
<div class="flex h-[500px]">
<!-- Sidebar -->
<div class="flex w-48 flex-col border-r border-stroke bg-surface-0 p-4">
<Text variant="header-1" class="mb-4">{$translate('settings.title')}</Text>
<nav class="space-y-1">
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'general'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'general')}
>
<Icon name="sliders-horizontal" class="h-4 w-4" />
{$translate('settings.tabs.general')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'appearance'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'appearance')}
>
<Icon name="palette" class="h-4 w-4" />
{$translate('settings.tabs.appearance')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'discovery'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'discovery')}
>
<Icon name="globe" class="h-4 w-4" />
{$translate('settings.tabs.discovery')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'library'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'library')}
>
<Icon name="library" class="h-4 w-4" />
{$translate('settings.tabs.library')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'sound'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'sound')}
>
<Icon name="volume-full" class="h-4 w-4" fill />
{$translate('settings.tabs.sound')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'diagnostics'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'diagnostics')}
>
<Icon name="terminal" class="h-4 w-4" />
{$translate('settings.tabs.diagnostics')}
</button>
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === 'about'
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = 'about')}
>
<Icon name="info" class="h-4 w-4" />
{$translate('settings.tabs.about')}
</button>
</nav>
</div>

<!-- Content -->
<div bind:this={contentEl} class="flex-1 overflow-auto p-6">
{#if activePage === 'general'}
<GeneralTab />
{:else if activePage === 'appearance'}
<AppearanceTab />
{:else if activePage === 'discovery'}
<DiscoveryTab />
{:else if activePage === 'library'}
<LibraryTab />
{:else if activePage === 'sound'}
<SoundTab />
{:else if activePage === 'diagnostics'}
<DiagnosticsTab />
{:else if activePage === 'about'}
<AboutTab />
{/if}
</div>
</div>
<Modal {open} {onClose} size="xl" flush>
<div class="flex h-[500px]">
<!-- Sidebar -->
<div class="flex w-48 flex-col border-r border-stroke bg-surface-0 p-4">
<Text variant="header-1" class="mb-4">{$translate('settings.title')}</Text>
<nav class="space-y-1">
{#each tabs as tab (tab.page)}
<button
type="button"
class="flex w-full items-center gap-2 rounded-md px-3 py-2
text-sm font-medium hover:cursor-pointer {activePage === tab.page
? 'bg-brand-muted text-brand-primary'
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'}"
onclick={() => (activePage = tab.page)}
>
<Icon name={tab.icon} class="h-4 w-4" fill={tab.fill} />
{$translate(`settings.tabs.${tab.page}`)}
</button>
{/each}
</nav>
</div>

<!-- Footer -->
<div class="flex justify-end border-t border-stroke px-6 py-4">
<Button variant="secondary" onclick={onClose}>{$translate('common.close')}</Button>
</div>
<!-- Content -->
<div bind:this={contentEl} class="flex-1 overflow-auto p-6">
{#if activePage === 'general'}
<GeneralTab />
{:else if activePage === 'appearance'}
<AppearanceTab />
{:else if activePage === 'discovery'}
<DiscoveryTab />
{:else if activePage === 'library'}
<LibraryTab />
{:else if activePage === 'sound'}
<SoundTab />
{:else if activePage === 'diagnostics'}
<DiagnosticsTab />
{:else if activePage === 'about'}
<AboutTab />
{/if}
</div>
{/if}
</dialog>
</div>

{#snippet footer()}
<Button variant="secondary" onclick={onClose}>{$translate('common.close')}</Button>
{/snippet}
</Modal>
3 changes: 3 additions & 0 deletions src/lib/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,9 @@
"settingsSound": "Ton",
"settingsDiagnostics": "Diagnose",
"quit": "{appName} beenden",
"hide": "{appName} ausblenden",
"hideOthers": "Andere ausblenden",
"showAll": "Alle anzeigen",
"importTracks": "Tracks importieren...",
"addRelease": "Release hinzufügen...",
"refreshMetadata": "Metadaten aktualisieren",
Expand Down
Loading
Loading