multiple fixes to profile saving and windows specific UI bugs

This commit is contained in:
Boof2015
2026-03-29 16:38:14 -04:00
parent 87f1516b4e
commit 436c45fd3a
16 changed files with 1289 additions and 167 deletions
+7 -6
View File
@@ -6,6 +6,7 @@ import { useThemeStore } from '../stores/themeStore'
import type { ScopeKind } from '../../types/scope'
import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance'
import { SCOPE_KINDS } from '../../types/scope'
import ThemedSelect from './ThemedSelect'
const SCOPE_LABELS: Record<ScopeKind, string> = {
spectrum: 'Spectrum',
@@ -198,19 +199,19 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
<div className="bottom-bar__section-title">Theme</div>
<div className="bottom-bar__section-body">
<div className="bottom-bar__inline bottom-bar__inline--theme">
<select
className="settings-control__select"
<ThemedSelect
value={activeThemeId ?? ''}
onChange={(event) => {
void handleThemeChange(event.target.value)
}}
className="bottom-bar__select"
>
{themeEntries.map(([id, theme]) => (
<option key={id} value={id}>
{theme.name}
</option>
))}
</select>
</ThemedSelect>
<button
type="button"
className="settings-chip"
@@ -277,12 +278,12 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
<div className="bottom-bar__section-title">Audio Source</div>
<div className="bottom-bar__section-body">
<div className="bottom-bar__inline">
<select
className="settings-control__select"
<ThemedSelect
value={selectedSourceValue}
onChange={(event) => {
void handleSourceChange(event.target.value)
}}
className="bottom-bar__select"
>
<optgroup label="Output Devices">
{visibleSystemSources.map((source) => (
@@ -300,7 +301,7 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
))}
</optgroup>
) : null}
</select>
</ThemedSelect>
<div className={`settings-status-pill is-${captureStatus}`.trim()}>
<span className="settings-status-pill__dot" />
@@ -2,6 +2,7 @@ import type { CSSProperties, JSX, ReactNode } from 'react'
import type { ScopeKind } from '../../types/scope'
import { SCOPE_LABELS } from '../../types/scope'
import type { ScopeSettings } from '../../types/settings'
import ThemedSelect from './ThemedSelect'
function vectorscopeModeLabel(mode: ScopeSettings['vectorscope']['mode']): string {
switch (mode) {
@@ -111,13 +112,12 @@ function SelectControl({
return (
<label className="settings-control">
<span className="settings-control__label">{label}</span>
<select
className="settings-control__select"
<ThemedSelect
value={value}
onChange={(event) => onChange(event.target.value)}
>
{children}
</select>
</ThemedSelect>
</label>
)
}
+37
View File
@@ -0,0 +1,37 @@
import type { JSX, SelectHTMLAttributes } from 'react'
function ChevronIcon(): JSX.Element {
return (
<svg viewBox="0 0 16 16" aria-hidden="true">
<path
d="M4.5 6.5 8 10l3.5-3.5"
fill="none"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
interface ThemedSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
className?: string
}
export default function ThemedSelect({
className,
children,
...props
}: ThemedSelectProps): JSX.Element {
return (
<div className={['settings-control__select', 'themed-select', className].filter(Boolean).join(' ')}>
<select className="themed-select__control" {...props}>
{children}
</select>
<span className="themed-select__chevron" aria-hidden="true">
<ChevronIcon />
</span>
</div>
)
}
+11 -4
View File
@@ -79,6 +79,8 @@ function getErrorMessage(error: unknown, fallback: string): string {
export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): JSX.Element {
const profiles = useSettingsStore((s) => s.profiles)
const activeProfileId = useSettingsStore((s) => s.activeProfileId)
const hasUnsavedProfileChanges = useSettingsStore((s) => s.hasUnsavedProfileChanges)
const guardProfileTransition = useSettingsStore((s) => s.guardProfileTransition)
const saveProfile = useSettingsStore((s) => s.saveProfile)
const loadProfile = useSettingsStore((s) => s.loadProfile)
const deleteProfile = useSettingsStore((s) => s.deleteProfile)
@@ -163,23 +165,27 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
const handleLoadProfile = useCallback(async (id: string) => {
try {
await loadProfile(id)
await guardProfileTransition(async () => {
await loadProfile(id)
})
} catch (error) {
window.alert(getErrorMessage(error, 'Could not load the profile.'))
} finally {
setIsProfileMenuOpen(false)
}
}, [loadProfile])
}, [guardProfileTransition, loadProfile])
const handleImportProfile = useCallback(async () => {
try {
await importProfileFromDialog()
await guardProfileTransition(async () => {
await importProfileFromDialog()
})
} catch (error) {
window.alert(getErrorMessage(error, 'Could not import the profile file.'))
} finally {
setIsProfileMenuOpen(false)
}
}, [importProfileFromDialog])
}, [guardProfileTransition, importProfileFromDialog])
const handleShowProfilesFolder = useCallback(async () => {
try {
@@ -314,6 +320,7 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
<span className="toolbar__profile-name">
{activeProfile?.name ?? 'Profiles'}
</span>
{hasUnsavedProfileChanges ? <span className="toolbar__profile-dirty-dot" aria-hidden="true" /> : null}
<ChevronIcon />
</button>
</div>
@@ -0,0 +1,65 @@
import { useCallback, useEffect, type JSX, type PointerEvent as ReactPointerEvent } from 'react'
import { RESIZE_DIRECTIONS, type ResizeDirection } from '../../types/windowResize'
const RESIZE_HANDLE_CLASSNAMES: Record<ResizeDirection, string> = {
n: 'window-resize-overlay__handle window-resize-overlay__handle--n',
s: 'window-resize-overlay__handle window-resize-overlay__handle--s',
e: 'window-resize-overlay__handle window-resize-overlay__handle--e',
w: 'window-resize-overlay__handle window-resize-overlay__handle--w',
ne: 'window-resize-overlay__handle window-resize-overlay__handle--ne',
nw: 'window-resize-overlay__handle window-resize-overlay__handle--nw',
se: 'window-resize-overlay__handle window-resize-overlay__handle--se',
sw: 'window-resize-overlay__handle window-resize-overlay__handle--sw',
}
export default function WindowResizeOverlay(): JSX.Element | null {
const isWindows = window.electronAPI.platform === 'win32'
const stopResize = useCallback(() => {
window.electronAPI.stopWindowResize()
}, [])
useEffect(() => {
if (!isWindows) return
window.addEventListener('blur', stopResize)
return () => {
window.removeEventListener('blur', stopResize)
stopResize()
}
}, [isWindows, stopResize])
const handlePointerDown = useCallback((direction: ResizeDirection) => {
return (event: ReactPointerEvent<HTMLDivElement>): void => {
if (event.button !== 0) return
event.preventDefault()
event.stopPropagation()
event.currentTarget.setPointerCapture(event.pointerId)
window.electronAPI.startWindowResize(direction)
}
}, [])
const handlePointerEnd = useCallback((event: ReactPointerEvent<HTMLDivElement>): void => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId)
}
stopResize()
}, [stopResize])
if (!isWindows) return null
return (
<div className="window-resize-overlay" aria-hidden="true">
{RESIZE_DIRECTIONS.map((direction) => (
<div
key={direction}
className={RESIZE_HANDLE_CLASSNAMES[direction]}
onPointerDown={handlePointerDown(direction)}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
onLostPointerCapture={handlePointerEnd}
/>
))}
</div>
)
}