mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 08:10:40 +02:00
multiple fixes to profile saving and windows specific UI bugs
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user