diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index db57424..a9fefec 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -233,3 +233,17 @@ pub async fn get_meter_levels( .map_err(|e| e.to_string()) }) } + +#[tauri::command] +pub async fn set_current_preset_name( + state: State<'_, AppState>, + id: u64, + name: String, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_current_preset_name(&name) + .map_err(|e| e.to_string()) + }) +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1f02d22..52c6f67 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,6 +19,7 @@ pub fn run() { commands::set_channel_inverse_gain, commands::recall_preset, commands::get_meter_levels, + commands::set_current_preset_name, ] ) .run(tauri::generate_context!()) diff --git a/src/App.tsx b/src/App.tsx index 9f66a64..6463494 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,14 @@ function App() { const [selected, setSelected] = useState(null); const [page, setPage] = useState('home'); const { notifications, notify, removeNotification, hoverNotification } = useNotifications(); + const activeDsp = dsps.find((d) => d.id === selected) ?? null; + const connectedDspCount = dsps.filter((dsp) => dsp.status === 'connected').length; + + useDSPPolling(dsps, selected, setDsps); + + function setStatus(id: number, status: DSPStatus) { + setDsps((prev) => prev.map((d) => (d.id === id ? { ...d, status } : d))); + } async function addDSP(dsp: Omit) { try { @@ -100,14 +108,6 @@ function App() { } } - function setStatus(id: number, status: DSPStatus) { - setDsps((prev) => prev.map((d) => (d.id === id ? { ...d, status } : d))); - } - - const activeDsp = dsps.find((d) => d.id === selected) ?? null; - const connectedDspCount = dsps.filter((dsp) => dsp.status === 'connected').length; - useDSPPolling(dsps, selected, setDsps); - return (
+ {children} + +
+
+ {label} +
+ + ); +} + +export default LoadingOverlay; diff --git a/src/components/Sidebar/Sidebar.css b/src/components/Sidebar/Sidebar.css index b70a14f..5c8757d 100644 --- a/src/components/Sidebar/Sidebar.css +++ b/src/components/Sidebar/Sidebar.css @@ -41,38 +41,6 @@ padding-inline: var(--space-3); } -.sidebar-app-name { - display: flex; - align-items: center; - aspect-ratio: 1; - height: calc(var(--header-height) * 0.7); - - font-size: var(--font-md); - font-weight: var(--font-weight-semibold); - color: var(--text-primary); - - white-space: nowrap; - overflow: hidden; - - flex: 0 1 auto; - min-width: 0; - - opacity: 0; - max-width: 0; - pointer-events: none; - - transition: - opacity var(--dur-fast) var(--ease-standard), - max-width var(--dur-base) var(--ease-standard); -} - -.sidebar-rail--expanded .sidebar-app-name { - opacity: 1; - max-width: 200px; - pointer-events: auto; - transition-delay: var(--dur-fast); -} - .sidebar-toggle { display: flex; align-items: center; @@ -113,6 +81,135 @@ transform: rotate(180deg); } +.sidebar-app-combobox { + position: relative; + flex: 1 1 auto; + min-width: 0; + + opacity: 0; + max-width: 0; + pointer-events: none; + + transition: + opacity var(--dur-fast) var(--ease-standard), + max-width var(--dur-base) var(--ease-standard); +} + +.sidebar-rail--expanded .sidebar-app-combobox { + opacity: 1; + max-width: 200px; + pointer-events: auto; + transition-delay: var(--dur-fast); +} + +.sidebar-app-trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + + height: calc(var(--header-height) * 0.6); + padding: 0 var(--space-2); + + background: var(--bg-panel); + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-sm); + + color: var(--text-primary); + font-family: var(--font-ui); + font-size: var(--font-xs); + font-weight: var(--font-weight-semibold); + + cursor: pointer; + transition: border-color var(--dur-base) var(--ease-standard); +} + +.sidebar-app-trigger:hover, +.sidebar-app-trigger--open { + border-color: var(--accent-brand); +} + +.sidebar-app-trigger-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-app-trigger-caret { + flex-shrink: 0; + color: var(--text-muted); + transition: transform var(--dur-base) var(--ease-standard); +} + +.sidebar-app-trigger--open .sidebar-app-trigger-caret { + transform: rotate(180deg); +} + +.sidebar-app-input { + width: 100%; + height: calc(var(--header-height) * 0.6); + padding: 0 var(--space-2); + + background: var(--bg-panel); + border: var(--border-width) solid var(--accent-brand); + border-radius: var(--radius-sm); + + color: var(--text-primary); + font-family: var(--font-ui); + font-size: var(--font-xs); + font-weight: var(--font-weight-semibold); + + outline: none; +} + +.sidebar-app-listbox { + position: absolute; + top: calc(100% + var(--space-1)); + left: 0; + right: 0; + z-index: 40; + + margin: 0; + padding: var(--space-1); + list-style: none; + + background: var(--bg-panel); + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-sm); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + + max-height: 240px; + overflow-y: auto; +} + +.sidebar-app-option { + padding: var(--space-2); + border-radius: var(--radius-sm); + font-size: var(--font-xs); + color: var(--text-primary); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-app-option--highlight { + background: var(--bg-raised); +} + +.sidebar-app-option--selected { + color: var(--accent-brand); + font-weight: var(--font-weight-semibold); +} +.sidebar-rail--expanded .sidebar-app-select { + opacity: 1; + max-width: 200px; + padding-inline: var(--space-2); + pointer-events: auto; + transition-delay: var(--dur-fast); +} + /* ---------- Scroll ---------- */ .sidebar-scroll { @@ -264,132 +361,3 @@ .app-root--sidebar-expanded .stage--with-sidebar { margin-left: var(--sidebar-width-expanded); } - -.sidebar-app-combobox { - position: relative; - flex: 1 1 auto; - min-width: 0; - - opacity: 0; - max-width: 0; - pointer-events: none; - - transition: - opacity var(--dur-fast) var(--ease-standard), - max-width var(--dur-base) var(--ease-standard); -} - -.sidebar-rail--expanded .sidebar-app-combobox { - opacity: 1; - max-width: 200px; - pointer-events: auto; - transition-delay: var(--dur-fast); -} - -.sidebar-app-trigger { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-2); - - height: calc(var(--header-height) * 0.6); - padding: 0 var(--space-2); - - background: var(--bg-panel); - border: var(--border-width) solid var(--border-hairline); - border-radius: var(--radius-sm); - - color: var(--text-primary); - font-family: var(--font-ui); - font-size: var(--font-xs); - font-weight: var(--font-weight-semibold); - - cursor: pointer; - transition: border-color var(--dur-base) var(--ease-standard); -} - -.sidebar-app-trigger:hover, -.sidebar-app-trigger--open { - border-color: var(--accent-brand); -} - -.sidebar-app-trigger-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.sidebar-app-trigger-caret { - flex-shrink: 0; - color: var(--text-muted); - transition: transform var(--dur-base) var(--ease-standard); -} - -.sidebar-app-trigger--open .sidebar-app-trigger-caret { - transform: rotate(180deg); -} - -.sidebar-app-input { - width: 100%; - height: calc(var(--header-height) * 0.6); - padding: 0 var(--space-2); - - background: var(--bg-panel); - border: var(--border-width) solid var(--accent-brand); - border-radius: var(--radius-sm); - - color: var(--text-primary); - font-family: var(--font-ui); - font-size: var(--font-xs); - font-weight: var(--font-weight-semibold); - - outline: none; -} - -.sidebar-app-listbox { - position: absolute; - top: calc(100% + var(--space-1)); - left: 0; - right: 0; - z-index: 40; - - margin: 0; - padding: var(--space-1); - list-style: none; - - background: var(--bg-panel); - border: var(--border-width) solid var(--border-hairline); - border-radius: var(--radius-sm); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - - max-height: 240px; - overflow-y: auto; -} - -.sidebar-app-option { - padding: var(--space-2); - border-radius: var(--radius-sm); - font-size: var(--font-xs); - color: var(--text-primary); - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar-app-option--highlight { - background: var(--bg-raised); -} - -.sidebar-app-option--selected { - color: var(--accent-brand); - font-weight: var(--font-weight-semibold); -} -.sidebar-rail--expanded .sidebar-app-select { - opacity: 1; - max-width: 200px; - padding-inline: var(--space-2); - pointer-events: auto; - transition-delay: var(--dur-fast); -} diff --git a/src/pages/DspPage/DspPage.css b/src/pages/DspPage/DspPage.css index c698ea5..d5bab92 100644 --- a/src/pages/DspPage/DspPage.css +++ b/src/pages/DspPage/DspPage.css @@ -2,12 +2,24 @@ position: fixed; left: var(--sidebar-width-collapsed); right: 0; - top: 0; bottom: 0; transition: left var(--dur-base) var(--ease-standard); - top: var(--header-height, 0); + top: var(--header-height); } .app-root:has(.sidebar-rail--expanded) .dsp-content { left: var(--sidebar-width-expanded); } + +.dsp-loading-overlay { + position: fixed; + left: var(--sidebar-width-collapsed); + right: 0; + top: var(--header-height, 0); + bottom: 0; + transition: left var(--dur-base) var(--ease-standard); +} + +.app-root:has(.sidebar-rail--expanded) .dsp-loading-overlay { + left: var(--sidebar-width-expanded); +} diff --git a/src/pages/DspPage/DspPage.tsx b/src/pages/DspPage/DspPage.tsx index 32d2f56..a37edf4 100644 --- a/src/pages/DspPage/DspPage.tsx +++ b/src/pages/DspPage/DspPage.tsx @@ -8,6 +8,7 @@ import { NotificationType } from '../../types/types'; import './DspPage.css'; import GateSection from '../../components/dsp408/GateSection/GateSection'; import { invoke } from '@tauri-apps/api/core'; +import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay'; function DSPPage({ dsp, @@ -22,7 +23,8 @@ function DSPPage({ }) { const [activeSidebar, setActiveSidebar] = useState('overview'); const [sidebarExpanded, setSidebarExpanded] = useState(false); - + const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0); + const [presetLoading, setPresetLoading] = useState(false); const sidebarItems = [ { id: 'overview', @@ -130,21 +132,27 @@ function DSPPage({ icon: , }, ]; - - const presets = dsp.state?.presets.names?.map((name, index) => ({ - id: index === 0 ? 'f00' : `u${String(index).padStart(2, '0')}`, - name, - })) ?? [ - { - id: 'f00', - name: 'Factory Preset', - }, - ...Array.from({ length: 20 }, (_, index) => ({ - id: `u${String(index + 1).padStart(2, '0')}`, - name: 'Default Preset', - })), - ]; - const selectedPreset = dsp.state?.presets.current_index ?? 0; + const presets = dsp.state?.presets.names + ? [ + { + id: 'f00', + name: 'Factory Preset', + }, + ...dsp.state.presets.names.map((name, index) => ({ + id: `u${String(index + 1).padStart(2, '0')}`, + name, + })), + ] + : [ + { + id: 'f00', + name: 'Factory Preset', + }, + ...Array.from({ length: 20 }, (_, index) => ({ + id: `u${String(index + 1).padStart(2, '0')}`, + name: 'Default Preset', + })), + ]; const recallPreset = async (presetIndex: number) => { try { @@ -166,6 +174,38 @@ function DSPPage({ } }; + const setCurrentPresetName = async (name: string) => { + try { + const success = await invoke('set_current_preset_name', { + id: dsp.id, + name, + }); + + if (success) { + notify(`Preset renamed`, 'success', dsp.name); + } else { + notify('Failed to rename preset', 'error', dsp.name); + } + + return success; + } catch (err) { + notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }; + + const onAppChange = async (id: string) => { + const presetIndex = presets.findIndex((preset) => preset.id === id); + if (presetIndex === -1) return; + + setPresetLoading(true); + const success = await recallPreset(presetIndex); + if (success) { + setSelectedPreset(presetIndex); + } + setPresetLoading(false); + }; + const renderTab = () => { switch (activeSidebar) { case 'overview': @@ -191,18 +231,19 @@ function DSPPage({ appName={dsp.name} appOptions={presets} selectedAppId={presets[selectedPreset]?.id} - onAppChange={(id) => { - const presetIndex = presets.findIndex((preset) => preset.id === id); - - if (presetIndex !== -1) { - recallPreset(presetIndex); - } - }} + onAppChange={onAppChange} + onAppRename={setCurrentPresetName} expanded={sidebarExpanded} onToggle={() => setSidebarExpanded((v) => !v)} /> -
{renderTab()}
+ +
{renderTab()}
+
); }