mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 08:10:40 +02:00
profile system, improved UI/UX
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import { useEffect, type CSSProperties, type JSX } from 'react'
|
||||
import { useAudioStore } from '../stores/audioStore'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore'
|
||||
import type { ScopeKind } from '../../types/scope'
|
||||
import { SCOPE_KINDS } from '../../types/scope'
|
||||
|
||||
const SCOPE_LABELS: Record<ScopeKind, string> = {
|
||||
spectrum: 'Spectrum',
|
||||
oscilloscope: 'Oscilloscope',
|
||||
vectorscope: 'Vectorscope',
|
||||
spectrogram: 'Spectrogram',
|
||||
vumeter: 'VU Meter',
|
||||
lufsmeter: 'LUFS Meter',
|
||||
waveform: 'Waveform',
|
||||
}
|
||||
|
||||
interface BottomBarProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function BottomBar({ onClose }: BottomBarProps): JSX.Element {
|
||||
|
||||
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
|
||||
const toggleScope = useSettingsStore((s) => s.toggleScope)
|
||||
const { presetId, accent, setPreset, setCustomAccent, customAccent } = useThemeStore()
|
||||
|
||||
const {
|
||||
systemSources,
|
||||
devices,
|
||||
selectedSystemSourceId,
|
||||
selectedDeviceId,
|
||||
captureMode,
|
||||
isCapturing,
|
||||
captureStatus,
|
||||
captureError,
|
||||
inputGainDb,
|
||||
refreshSystemSources,
|
||||
refreshDevices,
|
||||
refreshBackendSupport,
|
||||
selectSystemSource,
|
||||
selectDevice,
|
||||
startCapture,
|
||||
setInputGain,
|
||||
} = useAudioStore()
|
||||
|
||||
useEffect(() => {
|
||||
void refreshBackendSupport()
|
||||
void refreshSystemSources()
|
||||
void refreshDevices()
|
||||
}, [refreshBackendSupport, refreshSystemSources, refreshDevices])
|
||||
|
||||
const handleSourceChange = async (value: string): Promise<void> => {
|
||||
if (value.startsWith('system:')) {
|
||||
const sourceId = value.slice('system:'.length)
|
||||
await selectSystemSource(sourceId)
|
||||
await startCapture()
|
||||
return
|
||||
}
|
||||
|
||||
if (value.startsWith('device:')) {
|
||||
const deviceId = value.slice('device:'.length)
|
||||
await selectDevice(deviceId)
|
||||
await startCapture()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSourceValue = captureMode === 'system'
|
||||
? `system:${selectedSystemSourceId ?? systemSources[0]?.id ?? '__default_system_output__'}`
|
||||
: `device:${selectedDeviceId ?? ''}`
|
||||
|
||||
const visibleSystemSources = systemSources.length
|
||||
? systemSources
|
||||
: [{ id: '__default_system_output__', label: 'System Output', kind: 'system', isDefault: true }]
|
||||
|
||||
const showInputDevices = devices.length > 0
|
||||
|
||||
const indicatorLabel = isCapturing
|
||||
? 'Capturing'
|
||||
: captureStatus === 'connecting'
|
||||
? 'Connecting'
|
||||
: captureStatus === 'error'
|
||||
? 'Capture Failed'
|
||||
: 'Idle'
|
||||
|
||||
return (
|
||||
<div className="bottom-bar">
|
||||
<section className="bottom-bar__section">
|
||||
<div className="bottom-bar__section-title">Modules</div>
|
||||
<div className="bottom-bar__inline">
|
||||
{SCOPE_KINDS.map((kind) => {
|
||||
const active = !hiddenScopes.has(kind)
|
||||
return (
|
||||
<button
|
||||
key={kind}
|
||||
type="button"
|
||||
className={`settings-chip ${active ? 'is-active' : ''}`.trim()}
|
||||
onClick={() => toggleScope(kind)}
|
||||
title={SCOPE_LABELS[kind]}
|
||||
>
|
||||
{SCOPE_LABELS[kind]}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="bottom-bar__divider" />
|
||||
|
||||
<section className="bottom-bar__section">
|
||||
<div className="bottom-bar__section-title">Theme</div>
|
||||
<div className="bottom-bar__inline">
|
||||
{PRESET_IDS.map((id) => {
|
||||
const preset = PRESETS[id]
|
||||
const active = presetId === id && !customAccent
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`settings-swatch ${active ? 'is-active' : ''}`.trim()}
|
||||
style={{ '--swatch-color': preset.accent } as CSSProperties}
|
||||
onClick={() => setPreset(id)}
|
||||
title={preset.name}
|
||||
aria-label={preset.name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<input
|
||||
className="settings-accent-input"
|
||||
type="color"
|
||||
value={accent}
|
||||
onChange={(event) => setCustomAccent(event.target.value)}
|
||||
title="Custom accent color"
|
||||
/>
|
||||
{customAccent && (
|
||||
<button
|
||||
type="button"
|
||||
className="settings-chip"
|
||||
onClick={() => setCustomAccent(null)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="bottom-bar__divider" />
|
||||
|
||||
<section className="bottom-bar__section">
|
||||
<div className="bottom-bar__section-title">Audio Source</div>
|
||||
<div className="bottom-bar__inline">
|
||||
<select
|
||||
className="settings-control__select"
|
||||
value={selectedSourceValue}
|
||||
onChange={(event) => {
|
||||
void handleSourceChange(event.target.value)
|
||||
}}
|
||||
>
|
||||
<optgroup label="Output Devices">
|
||||
{visibleSystemSources.map((source) => (
|
||||
<option key={source.id} value={`system:${source.id}`}>
|
||||
{source.isDefault ? `${source.label} (Default)` : source.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{showInputDevices ? (
|
||||
<optgroup label="Input Devices">
|
||||
{devices.map((device) => (
|
||||
<option key={device.deviceId} value={`device:${device.deviceId}`}>
|
||||
{device.label || `Input ${device.deviceId.slice(0, 8)}`}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
</select>
|
||||
|
||||
<div className={`settings-status-pill is-${captureStatus}`.trim()}>
|
||||
<span className="settings-status-pill__dot" />
|
||||
<span>{indicatorLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{captureError ? (
|
||||
<div className="settings-error-text">{captureError}</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="bottom-bar__divider" />
|
||||
|
||||
<section className="bottom-bar__section">
|
||||
<div className="bottom-bar__section-title">Trim</div>
|
||||
<div className="bottom-bar__inline">
|
||||
<span className="bottom-bar__trim-value">
|
||||
{inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB
|
||||
</span>
|
||||
<input
|
||||
className="settings-control__range bottom-bar__trim-slider"
|
||||
type="range"
|
||||
min={-12}
|
||||
max={12}
|
||||
step={0.5}
|
||||
value={inputGainDb}
|
||||
onChange={(event) => setInputGain(Number(event.target.value))}
|
||||
onDoubleClick={() => setInputGain(0)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="settings-panel__close"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, type CSSProperties, type JSX, type ReactNode } from 'react'
|
||||
import { useAudioStore } from '../stores/audioStore'
|
||||
import { useMemo, type CSSProperties, type JSX, type ReactNode } from 'react'
|
||||
import { useSettingsStore, type ScopeSettings } from '../stores/settingsStore'
|
||||
import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore'
|
||||
import type { ScopeKind } from '../../types/scope'
|
||||
import { buildAnalyzerGridTemplateColumns } from '../analyzerLayout'
|
||||
|
||||
@@ -470,31 +468,8 @@ function ScopeSettingsSection({
|
||||
)
|
||||
}
|
||||
|
||||
interface SettingsPanelProps {
|
||||
onClose: () => void
|
||||
onHeightChange?: (height: number) => void
|
||||
}
|
||||
|
||||
export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanelProps): JSX.Element {
|
||||
const {
|
||||
systemSources,
|
||||
devices,
|
||||
selectedSystemSourceId,
|
||||
selectedDeviceId,
|
||||
captureMode,
|
||||
isCapturing,
|
||||
captureStatus,
|
||||
captureError,
|
||||
refreshSystemSources,
|
||||
refreshDevices,
|
||||
refreshBackendSupport,
|
||||
selectSystemSource,
|
||||
selectDevice,
|
||||
startCapture,
|
||||
} = useAudioStore()
|
||||
export default function SettingsPanel(): JSX.Element {
|
||||
const { scopeSettings, updateScopeSettings, hiddenScopes, scopeOrder, widthWeights } = useSettingsStore()
|
||||
const { presetId, accent, setPreset, setCustomAccent, customAccent } = useThemeStore()
|
||||
const panelRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const visibleScopes = useMemo(
|
||||
() => scopeOrder.filter((kind) => !hiddenScopes.has(kind)),
|
||||
@@ -506,170 +481,8 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
|
||||
return { gridTemplateColumns } as CSSProperties
|
||||
}, [visibleScopes, widthWeights])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshBackendSupport()
|
||||
void refreshSystemSources()
|
||||
void refreshDevices()
|
||||
}, [refreshBackendSupport, refreshSystemSources, refreshDevices])
|
||||
|
||||
useEffect(() => {
|
||||
const panel = panelRef.current
|
||||
if (!panel || !onHeightChange) return
|
||||
|
||||
const reportHeight = (): void => {
|
||||
const nextHeight = Math.ceil(panel.getBoundingClientRect().height)
|
||||
onHeightChange(nextHeight)
|
||||
}
|
||||
|
||||
reportHeight()
|
||||
|
||||
const observer = typeof ResizeObserver === 'undefined'
|
||||
? null
|
||||
: new ResizeObserver(() => reportHeight())
|
||||
|
||||
observer?.observe(panel)
|
||||
window.addEventListener('resize', reportHeight)
|
||||
|
||||
return () => {
|
||||
observer?.disconnect()
|
||||
window.removeEventListener('resize', reportHeight)
|
||||
}
|
||||
}, [onHeightChange, visibleScopes.length])
|
||||
|
||||
const handleSourceChange = async (value: string): Promise<void> => {
|
||||
if (value.startsWith('system:')) {
|
||||
const sourceId = value.slice('system:'.length)
|
||||
await selectSystemSource(sourceId)
|
||||
await startCapture()
|
||||
return
|
||||
}
|
||||
|
||||
if (value.startsWith('device:')) {
|
||||
const deviceId = value.slice('device:'.length)
|
||||
await selectDevice(deviceId)
|
||||
await startCapture()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSourceValue = captureMode === 'system'
|
||||
? `system:${selectedSystemSourceId ?? systemSources[0]?.id ?? '__default_system_output__'}`
|
||||
: `device:${selectedDeviceId ?? ''}`
|
||||
|
||||
const visibleSystemSources = systemSources.length
|
||||
? systemSources
|
||||
: [{ id: '__default_system_output__', label: 'System Output', kind: 'system', isDefault: true }]
|
||||
|
||||
const showInputDevices = devices.length > 0
|
||||
|
||||
const renderSystemSourceLabel = (label: string, isDefault?: boolean): string => (
|
||||
isDefault ? `${label} (Default)` : label
|
||||
)
|
||||
|
||||
const renderInputDeviceValue = (deviceId: string): string => `device:${deviceId}`
|
||||
const renderSystemSourceValue = (sourceId: string): string => `system:${sourceId}`
|
||||
|
||||
const indicatorLabel = isCapturing
|
||||
? 'Capturing'
|
||||
: captureStatus === 'connecting'
|
||||
? 'Connecting'
|
||||
: captureStatus === 'error'
|
||||
? 'Capture Failed'
|
||||
: 'Idle'
|
||||
|
||||
return (
|
||||
<div className="settings-panel" ref={panelRef}>
|
||||
<div className="settings-panel__utility-row">
|
||||
<section className="settings-utility-section settings-utility-section--source">
|
||||
<div className="settings-section-title">Audio Source</div>
|
||||
|
||||
<label className="settings-control settings-control--stack">
|
||||
<span className="settings-control__label">Source</span>
|
||||
<select
|
||||
className="settings-control__select"
|
||||
value={selectedSourceValue}
|
||||
onChange={(event) => {
|
||||
void handleSourceChange(event.target.value)
|
||||
}}
|
||||
>
|
||||
<optgroup label="Output Devices">
|
||||
{visibleSystemSources.map((source) => (
|
||||
<option key={source.id} value={renderSystemSourceValue(source.id)}>
|
||||
{renderSystemSourceLabel(source.label, source.isDefault)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{showInputDevices ? (
|
||||
<optgroup label="Input Devices">
|
||||
{devices.map((device) => (
|
||||
<option key={device.deviceId} value={renderInputDeviceValue(device.deviceId)}>
|
||||
{device.label || `Input ${device.deviceId.slice(0, 8)}`}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className={`settings-status-pill is-${captureStatus}`.trim()}>
|
||||
<span className="settings-status-pill__dot" />
|
||||
<span>{indicatorLabel}</span>
|
||||
</div>
|
||||
|
||||
{captureError ? (
|
||||
<div className="settings-error-text">{captureError}</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="settings-utility-section settings-utility-section--theme">
|
||||
<div className="settings-section-title">Theme</div>
|
||||
|
||||
<div className="settings-theme-swatches">
|
||||
{PRESET_IDS.map((id) => {
|
||||
const preset = PRESETS[id]
|
||||
const active = presetId === id && !customAccent
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`settings-swatch ${active ? 'is-active' : ''}`.trim()}
|
||||
style={{ '--swatch-color': preset.accent } as CSSProperties}
|
||||
onClick={() => setPreset(id)}
|
||||
title={preset.name}
|
||||
aria-label={preset.name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<label className="settings-control settings-control--stack">
|
||||
<span className="settings-control__label">Custom Accent</span>
|
||||
<div className="settings-accent-row">
|
||||
<input
|
||||
className="settings-accent-input"
|
||||
type="color"
|
||||
value={accent}
|
||||
onChange={(event) => setCustomAccent(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="settings-chip"
|
||||
onClick={() => setCustomAccent(null)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="settings-panel__close"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="settings-panel">
|
||||
<div className="settings-panel__scope-track" style={scopeTrackStyle}>
|
||||
{visibleScopes.map((kind) => (
|
||||
<ScopeSettingsSection
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import { useState, useEffect, useCallback, type CSSProperties, type JSX } from 'react'
|
||||
import type { ScopeKind } from '../../types/scope'
|
||||
import { SCOPE_KINDS } from '../../types/scope'
|
||||
import { useState, useEffect, useCallback, useRef, type CSSProperties, type JSX } from 'react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
|
||||
const SCOPE_LABELS: Record<ScopeKind, string> = {
|
||||
spectrum: 'SPEC',
|
||||
oscilloscope: 'OSC',
|
||||
vectorscope: 'VEC',
|
||||
spectrogram: 'GRAM',
|
||||
vumeter: 'VU',
|
||||
lufsmeter: 'LUFS',
|
||||
waveform: 'WAVE',
|
||||
}
|
||||
|
||||
function SettingsIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
@@ -38,15 +26,63 @@ function CloseIcon(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function GripIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<circle cx="5.5" cy="4" r="1.2" fill="currentColor" />
|
||||
<circle cx="10.5" cy="4" r="1.2" fill="currentColor" />
|
||||
<circle cx="5.5" cy="8" r="1.2" fill="currentColor" />
|
||||
<circle cx="10.5" cy="8" r="1.2" fill="currentColor" />
|
||||
<circle cx="5.5" cy="12" r="1.2" fill="currentColor" />
|
||||
<circle cx="10.5" cy="12" r="1.2" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MinimizeIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3.5 8h9" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function RepositionIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M4 6l4-3 4 3M4 10l4 3 4-3" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" style={{ width: 10, height: 10 }}>
|
||||
<path d="M5 6l3 3 3-3" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface ToolbarProps {
|
||||
onOpenSettings: () => void
|
||||
settingsOpen: boolean
|
||||
}
|
||||
|
||||
export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): JSX.Element {
|
||||
const hiddenScopes = useSettingsStore((state) => state.hiddenScopes)
|
||||
const toggleScope = useSettingsStore((state) => state.toggleScope)
|
||||
const profiles = useSettingsStore((s) => s.profiles)
|
||||
const activeProfileId = useSettingsStore((s) => s.activeProfileId)
|
||||
const saveProfile = useSettingsStore((s) => s.saveProfile)
|
||||
const loadProfile = useSettingsStore((s) => s.loadProfile)
|
||||
const deleteProfile = useSettingsStore((s) => s.deleteProfile)
|
||||
const renameProfile = useSettingsStore((s) => s.renameProfile)
|
||||
const updateActiveProfile = useSettingsStore((s) => s.updateActiveProfile)
|
||||
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(true)
|
||||
const [showReposition, setShowReposition] = useState(false)
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false)
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null)
|
||||
const [renameValue, setRenameValue] = useState('')
|
||||
const profileMenuRef = useRef<HTMLDivElement>(null)
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.isAlwaysOnTop().then(setIsAlwaysOnTop)
|
||||
@@ -54,12 +90,72 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
// Close profile menu on outside click
|
||||
useEffect(() => {
|
||||
if (!showProfileMenu) return
|
||||
const handleClick = (e: MouseEvent): void => {
|
||||
if (profileMenuRef.current && !profileMenuRef.current.contains(e.target as Node)) {
|
||||
setShowProfileMenu(false)
|
||||
setRenamingId(null)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [showProfileMenu])
|
||||
|
||||
// Focus rename input when it appears
|
||||
useEffect(() => {
|
||||
if (renamingId && renameInputRef.current) {
|
||||
renameInputRef.current.focus()
|
||||
renameInputRef.current.select()
|
||||
}
|
||||
}, [renamingId])
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
window.electronAPI.toggleAlwaysOnTop()
|
||||
}, [])
|
||||
|
||||
const handleReposition = useCallback((position: 'top' | 'bottom') => {
|
||||
window.electronAPI.repositionWindow(position)
|
||||
setShowReposition(false)
|
||||
}, [])
|
||||
|
||||
const handleSaveNew = useCallback(() => {
|
||||
const count = Object.keys(profiles).length
|
||||
saveProfile(`Profile ${count}`)
|
||||
setShowProfileMenu(false)
|
||||
}, [profiles, saveProfile])
|
||||
|
||||
const handleSaveOverwrite = useCallback(() => {
|
||||
updateActiveProfile()
|
||||
setShowProfileMenu(false)
|
||||
}, [updateActiveProfile])
|
||||
|
||||
const handleStartRename = useCallback((id: string, currentName: string) => {
|
||||
setRenamingId(id)
|
||||
setRenameValue(currentName)
|
||||
}, [])
|
||||
|
||||
const handleFinishRename = useCallback(() => {
|
||||
if (renamingId && renameValue.trim()) {
|
||||
renameProfile(renamingId, renameValue.trim())
|
||||
}
|
||||
setRenamingId(null)
|
||||
}, [renamingId, renameValue, renameProfile])
|
||||
|
||||
const profileIds = Object.keys(profiles)
|
||||
const activeProfile = activeProfileId ? profiles[activeProfileId] : null
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div
|
||||
className="toolbar__grab"
|
||||
style={{ WebkitAppRegion: 'drag' } as CSSProperties}
|
||||
title="Drag to move window"
|
||||
>
|
||||
<GripIcon />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="toolbar__brand"
|
||||
style={{ WebkitAppRegion: 'drag' } as CSSProperties}
|
||||
@@ -68,23 +164,145 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
|
||||
<span className="toolbar__brand-text">Prism</span>
|
||||
</div>
|
||||
|
||||
<div className="toolbar__chips">
|
||||
{SCOPE_KINDS.map((kind) => {
|
||||
const active = !hiddenScopes.has(kind)
|
||||
return (
|
||||
<div className="toolbar__profile" ref={profileMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`toolbar__profile-button ${showProfileMenu ? 'is-active' : ''}`.trim()}
|
||||
onClick={() => setShowProfileMenu((prev) => !prev)}
|
||||
title="Presets"
|
||||
>
|
||||
<span className="toolbar__profile-name">
|
||||
{activeProfile?.name ?? 'Presets'}
|
||||
</span>
|
||||
<ChevronIcon />
|
||||
</button>
|
||||
|
||||
{showProfileMenu && (
|
||||
<div className="toolbar__profile-menu">
|
||||
<div className="toolbar__profile-menu-section">
|
||||
<div className="toolbar__profile-menu-label">Presets</div>
|
||||
{profileIds.map((id) => {
|
||||
const profile = profiles[id]
|
||||
const isActive = id === activeProfileId
|
||||
const isDefault = id === 'profile_default'
|
||||
|
||||
if (renamingId === id) {
|
||||
return (
|
||||
<div key={id} className="toolbar__profile-menu-item">
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
className="toolbar__profile-rename-input"
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onBlur={handleFinishRename}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleFinishRename()
|
||||
if (e.key === 'Escape') setRenamingId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`toolbar__profile-menu-item ${isActive ? 'is-active' : ''}`.trim()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__profile-menu-item-name"
|
||||
onClick={() => {
|
||||
loadProfile(id)
|
||||
setShowProfileMenu(false)
|
||||
}}
|
||||
>
|
||||
{isActive && <span className="toolbar__profile-check">✓</span>}
|
||||
{profile.name}
|
||||
</button>
|
||||
{!isDefault && (
|
||||
<div className="toolbar__profile-menu-item-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__profile-menu-action"
|
||||
onClick={() => handleStartRename(id, profile.name)}
|
||||
title="Rename"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__profile-menu-action toolbar__profile-menu-action--danger"
|
||||
onClick={() => deleteProfile(id)}
|
||||
title="Delete"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="toolbar__profile-menu-divider" />
|
||||
|
||||
<button
|
||||
key={kind}
|
||||
type="button"
|
||||
className={`toolbar__chip ${active ? 'is-active' : ''}`.trim()}
|
||||
onClick={() => toggleScope(kind)}
|
||||
className="toolbar__profile-menu-action-row"
|
||||
onClick={handleSaveNew}
|
||||
>
|
||||
{SCOPE_LABELS[kind]}
|
||||
Save as New Preset
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{activeProfileId && (
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__profile-menu-action-row"
|
||||
onClick={handleSaveOverwrite}
|
||||
>
|
||||
Save to "{activeProfile?.name}"
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="toolbar__spacer"
|
||||
style={{ WebkitAppRegion: 'drag' } as CSSProperties}
|
||||
/>
|
||||
|
||||
<div className="toolbar__actions">
|
||||
<div className="toolbar__reposition-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className={`toolbar__icon-button ${showReposition ? 'is-active' : ''}`.trim()}
|
||||
onClick={() => setShowReposition((prev) => !prev)}
|
||||
title="Reposition window"
|
||||
aria-label="Reposition window"
|
||||
>
|
||||
<RepositionIcon />
|
||||
</button>
|
||||
{showReposition && (
|
||||
<div className="toolbar__reposition-menu">
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__reposition-option"
|
||||
onClick={() => handleReposition('top')}
|
||||
>
|
||||
Top
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__reposition-option"
|
||||
onClick={() => handleReposition('bottom')}
|
||||
>
|
||||
Bottom
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`toolbar__icon-button ${settingsOpen ? 'is-active' : ''}`.trim()}
|
||||
@@ -105,6 +323,16 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
|
||||
<PinIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__icon-button"
|
||||
onClick={() => window.electronAPI.minimize()}
|
||||
title="Minimize"
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<MinimizeIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="toolbar__icon-button toolbar__icon-button--danger"
|
||||
|
||||
Reference in New Issue
Block a user