diff --git a/src/main/index.ts b/src/main/index.ts index 92e666d..bb88b3e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -575,52 +575,6 @@ function setWindowAlwaysOnTop(window: BrowserWindow, next: boolean): void { }) } -function attachWindowShortcuts(window: BrowserWindow): void { - const scopeKeys = new Map([ - ['1', 0], - ['2', 1], - ['3', 2], - ['4', 3], - ['5', 4], - ['6', 5], - ['7', 6], - ['8', 7], - ]) - - window.webContents.on('before-input-event', (_event, input: Electron.Input) => { - if (input.type !== 'keyDown') { - return - } - - const isMainWindow = isMainRendererWindow(window) - - if (!input.alt && !input.control && !input.meta && !input.shift) { - if (isMainWindow) { - const scopeIndex = scopeKeys.get(input.key) - if (scopeIndex !== undefined) { - window.webContents.send('shortcut:toggle-scope', scopeIndex) - return - } - - if (input.key === ' ') { - window.webContents.send('shortcut:toggle-capture') - return - } - } - - if (input.key === 't') { - setWindowAlwaysOnTop(window, !window.isAlwaysOnTop()) - } - - return - } - - if (isMainWindow && input.key === ',' && input.meta && !input.alt && !input.control && !input.shift) { - window.webContents.send('shortcut:toggle-settings') - } - }) -} - function loadRendererTarget(window: BrowserWindow, query: Record): void { if (process.env.ELECTRON_RENDERER_URL) { const url = new URL(process.env.ELECTRON_RENDERER_URL) @@ -746,7 +700,6 @@ function createMainWindow(): void { scheduleMainWindowBoundsSave(mainWindow) }) - attachWindowShortcuts(mainWindow) loadRendererTarget(mainWindow, { window: 'main' }) } @@ -877,7 +830,6 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro popoutWindow.on('move', () => emitPopoutBoundsChanged(kind, popoutWindow)) popoutWindow.on('resize', () => emitPopoutBoundsChanged(kind, popoutWindow)) - attachWindowShortcuts(popoutWindow) loadRendererTarget(popoutWindow, { window: 'scope-popout', scope: kind }) return popoutWindow } diff --git a/src/preload/index.ts b/src/preload/index.ts index b9e8ba3..0658e7c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -98,21 +98,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('window:always-on-top-changed', handler) return () => ipcRenderer.removeListener('window:always-on-top-changed', handler) }, - onToggleScope: (callback: (index: number) => void) => { - const handler = (_event: Electron.IpcRendererEvent, index: number): void => callback(index) - ipcRenderer.on('shortcut:toggle-scope', handler) - return () => ipcRenderer.removeListener('shortcut:toggle-scope', handler) - }, - onToggleCapture: (callback: () => void) => { - const handler = (): void => callback() - ipcRenderer.on('shortcut:toggle-capture', handler) - return () => ipcRenderer.removeListener('shortcut:toggle-capture', handler) - }, - onToggleSettings: (callback: () => void) => { - const handler = (): void => callback() - ipcRenderer.on('shortcut:toggle-settings', handler) - return () => ipcRenderer.removeListener('shortcut:toggle-settings', handler) - }, onMainWindowBoundsChanged: (callback: (bounds: WindowBounds) => void) => { const handler = (_event: Electron.IpcRendererEvent, bounds: WindowBounds): void => callback(bounds) ipcRenderer.on('window:bounds-changed', handler) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 83a7d2b..8dd1a5e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -5,31 +5,35 @@ import SettingsPanel from './components/SettingsPanel' import BottomBar from './components/BottomBar' import ScopePopoutBridge from './components/ScopePopoutBridge' import WindowResizeOverlay from './components/WindowResizeOverlay' +import AppBanner from './components/AppBanner' import { useSettingsStore } from './stores/settingsStore' import { useAstraStore } from './stores/astraStore' import { useAudioStore } from './stores/audioStore' import { useThemeStore } from './stores/themeStore' -import { SCOPE_KINDS } from '../types/scope' +import { useUiStore } from './stores/uiStore' const DEFAULT_SETTINGS_HEIGHT = 400 export default function App(): JSX.Element { const [toolbarVisible, setToolbarVisible] = useState(false) - const [settingsOpen, setSettingsOpen] = useState(false) const [settingsPanelHeight, setSettingsPanelHeight] = useState(0) const [bottomBarHeight, setBottomBarHeight] = useState(0) const hideTimeoutRef = useRef | null>(null) const externalProfileOpenQueueRef = useRef(Promise.resolve()) - const toggleScope = useSettingsStore((s) => s.toggleScope) const initializeProfiles = useSettingsStore((s) => s.initializeProfiles) const applyExternalProfileSnapshot = useSettingsStore((s) => s.applyExternalProfileSnapshot) const guardProfileTransition = useSettingsStore((s) => s.guardProfileTransition) const importProfileFromPath = useSettingsStore((s) => s.importProfileFromPath) + const showProfilesFolder = useSettingsStore((s) => s.showProfilesFolder) const updateMainWindowBounds = useSettingsStore((s) => s.updateMainWindowBounds) const initializeThemes = useThemeStore((s) => s.initializeThemes) const applyExternalThemeSnapshot = useThemeStore((s) => s.applyExternalThemeSnapshot) const initializeAstra = useAstraStore((s) => s.initialize) + const settingsOpen = useUiStore((s) => s.settingsOpen) + const toggleSettings = useUiStore((s) => s.toggleSettings) + const setSettingsOpen = useUiStore((s) => s.setSettingsOpen) + const showBanner = useUiStore((s) => s.showBanner) // Auto-capture on launch useEffect(() => { @@ -71,9 +75,32 @@ export default function App(): JSX.Element { } }) .catch((error: unknown) => { - window.alert(error instanceof Error && error.message + const message = error instanceof Error && error.message ? error.message - : `Prism could not open ${path}.`) + : `Prism could not open ${path}.` + + showBanner({ + tone: 'error', + message, + actions: [ + { + label: 'Open Folder', + onSelect: async () => { + try { + await showProfilesFolder() + } catch (folderError) { + showBanner({ + tone: 'error', + message: folderError instanceof Error && folderError.message + ? folderError.message + : 'Could not open the profiles folder.', + actions: [], + }) + } + }, + }, + ], + }) }) }) const unsubscribeCloseRequested = window.electronAPI.onMainCloseRequested(() => { @@ -104,6 +131,8 @@ export default function App(): JSX.Element { initializeProfiles, initializeThemes, initializeAstra, + showBanner, + showProfilesFolder, updateMainWindowBounds, ]) @@ -133,12 +162,12 @@ export default function App(): JSX.Element { }, [settingsOpen]) const handleToggleSettings = useCallback(() => { - setSettingsOpen((prev) => !prev) - }, []) + toggleSettings() + }, [toggleSettings]) const handleCloseSettings = useCallback(() => { setSettingsOpen(false) - }, []) + }, [setSettingsOpen]) const handleAltDragStart = useCallback((event: React.MouseEvent) => { if (event.altKey && event.button === 0) { @@ -160,29 +189,6 @@ export default function App(): JSX.Element { } }, []) - // Keyboard shortcuts from main process - useEffect(() => { - const unsubs = [ - window.electronAPI.onToggleScope((index) => { - if (index >= 0 && index < SCOPE_KINDS.length) { - toggleScope(SCOPE_KINDS[index]) - } - }), - window.electronAPI.onToggleCapture(() => { - const { isCapturing, startCapture, stopCapture } = useAudioStore.getState() - if (isCapturing) { - stopCapture() - } else { - startCapture() - } - }), - window.electronAPI.onToggleSettings(() => { - setSettingsOpen((prev) => !prev) - }), - ] - return () => unsubs.forEach((unsub) => unsub()) - }, [toggleScope]) - return (
+ + {settingsOpen && (
diff --git a/src/renderer/audio/AudioCapture.ts b/src/renderer/audio/AudioCapture.ts index fe2f594..323e0c8 100644 --- a/src/renderer/audio/AudioCapture.ts +++ b/src/renderer/audio/AudioCapture.ts @@ -114,7 +114,7 @@ function toDeviceSourceDescriptor(device: MediaDeviceInfo): CaptureSourceDescrip function getDefaultSystemSourceDescriptor(): CaptureSourceDescriptor { return { id: DEFAULT_SYSTEM_SOURCE_ID, - label: 'System Output', + label: 'Default Output', kind: 'system', isDefault: true, } @@ -702,7 +702,8 @@ class AudioCapture { const activeSystemBackend = this.resolveCandidateBackends(this.backendSupport ?? DEFAULT_BACKEND_SUPPORT, 'system')[0] const sources = await activeSystemBackend.listSources() - return sources.length ? sources : [getDefaultSystemSourceDescriptor()] + const dedupedSources = sources.filter((source) => source.id !== DEFAULT_SYSTEM_SOURCE_ID) + return [getDefaultSystemSourceDescriptor(), ...dedupedSources] } async listDevices(): Promise { diff --git a/src/renderer/components/AppBanner.tsx b/src/renderer/components/AppBanner.tsx new file mode 100644 index 0000000..8d169f2 --- /dev/null +++ b/src/renderer/components/AppBanner.tsx @@ -0,0 +1,61 @@ +import { useState, type JSX } from 'react' +import { useUiStore, type UiBannerAction } from '../stores/uiStore' + +async function handleBannerAction( + action: UiBannerAction, + bannerId: number, + dismissBanner: (bannerId?: number) => void, + setPendingAction: (label: string | null) => void, +): Promise { + setPendingAction(action.label) + try { + await action.onSelect?.() + } finally { + setPendingAction(null) + if (action.dismissOnSelect ?? true) { + dismissBanner(bannerId) + } + } +} + +export default function AppBanner(): JSX.Element | null { + const banner = useUiStore((state) => state.banner) + const dismissBanner = useUiStore((state) => state.dismissBanner) + const [pendingAction, setPendingAction] = useState(null) + + if (!banner) { + return null + } + + return ( +
+
+
{banner.message}
+
+ {banner.actions.map((action) => ( + + ))} + +
+
+
+ ) +} diff --git a/src/renderer/components/AstraScopeModule.tsx b/src/renderer/components/AstraScopeModule.tsx index dfb26fd..a8f5774 100644 --- a/src/renderer/components/AstraScopeModule.tsx +++ b/src/renderer/components/AstraScopeModule.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, type CSSProperties, type JSX } from 'reac import type { ScopeSettings } from '../../types/settings' import type { ResolvedAstraTheme } from '../../types/theme' import { useAstraStore } from '../stores/astraStore' +import { useUiStore } from '../stores/uiStore' import { formatAstraTime, getAstraPlaybackProgress } from '../utils/astra' interface AstraScopeModuleProps { @@ -9,6 +10,12 @@ interface AstraScopeModuleProps { settings: ScopeSettings['astra'] } +function getErrorMessage(error: unknown, fallback: string): string { + return error instanceof Error && error.message + ? error.message + : fallback +} + function hasVisibleFields(settings: ScopeSettings['astra']): boolean { return settings.showCoverArt || settings.showTitle @@ -53,6 +60,9 @@ export default function AstraScopeModule({ const integrationState = useAstraStore((s) => s.integrationState) const isSendingControl = useAstraStore((s) => s.isSendingControl) const sendControl = useAstraStore((s) => s.sendControl) + const saveConfig = useAstraStore((s) => s.saveConfig) + const setSettingsOpen = useUiStore((s) => s.setSettingsOpen) + const showBanner = useUiStore((s) => s.showBanner) const [nowMs, setNowMs] = useState(() => Date.now()) useEffect(() => { @@ -212,11 +222,37 @@ export default function AstraScopeModule({ )} {errorMessage && ( -
- {errorMessage} -
+ <> +
+ {errorMessage} +
+
+ + +
+ )}
diff --git a/src/renderer/components/BottomBar.tsx b/src/renderer/components/BottomBar.tsx index 69ce8ed..92360d7 100644 --- a/src/renderer/components/BottomBar.tsx +++ b/src/renderer/components/BottomBar.tsx @@ -4,6 +4,7 @@ import { useAudioStore } from '../stores/audioStore' import { usePerformanceStore } from '../stores/performanceStore' import { useSettingsStore } from '../stores/settingsStore' import { useThemeStore } from '../stores/themeStore' +import { useUiStore } from '../stores/uiStore' import { getHorizontalWheelScrollResult } from '../utils/horizontalWheelScroll' import type { ScopeKind } from '../../types/scope' import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance' @@ -36,6 +37,14 @@ const FRAME_TARGET_LABELS: Record = { 'display-sync': 'Sync', } +const DEFAULT_INPUT_DEVICE_ID = '__default_input__' + +function getErrorMessage(error: unknown, fallback: string): string { + return error instanceof Error && error.message + ? error.message + : fallback +} + export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element { const rootRef = useRef(null) const [astraBaseUrlInput, setAstraBaseUrlInput] = useState('') @@ -66,7 +75,9 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): isCapturing, captureStatus, captureError, + captureNotice, inputGainDb, + clearCaptureNotice, refreshSystemSources, refreshDevices, refreshBackendSupport, @@ -75,6 +86,8 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): startCapture, setInputGain, } = useAudioStore() + const showBanner = useUiStore((s) => s.showBanner) + const setSettingsOpen = useUiStore((s) => s.setSettingsOpen) useEffect(() => { void refreshBackendSupport() @@ -124,20 +137,19 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): if (value.startsWith('device:')) { const deviceId = value.slice('device:'.length) - await selectDevice(deviceId) + await selectDevice(deviceId === DEFAULT_INPUT_DEVICE_ID ? null : 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 }] + : [{ id: '__default_system_output__', label: 'Default Output', kind: 'system', isDefault: true }] + const defaultSystemSourceId = visibleSystemSources[0]?.id ?? '__default_system_output__' - const showInputDevices = devices.length > 0 + const selectedSourceValue = captureMode === 'system' + ? `system:${selectedSystemSourceId ?? defaultSystemSourceId}` + : `device:${selectedDeviceId ?? DEFAULT_INPUT_DEVICE_ID}` const indicatorLabel = isCapturing ? 'Capturing' @@ -161,7 +173,46 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): baseUrl: astraBaseUrlInput, token: astraTokenInput, } - await saveAstraConfig(nextConfig) + try { + await saveAstraConfig(nextConfig) + } catch (error) { + showBanner({ + tone: 'error', + message: getErrorMessage(error, 'Could not save the Astra settings.'), + actions: [], + }) + } + } + + const handleRetryCapture = async (): Promise => { + clearCaptureNotice() + await startCapture() + } + + const handleUseDefaultSource = async (): Promise => { + clearCaptureNotice() + if (captureMode === 'system') { + await selectSystemSource(defaultSystemSourceId) + } else { + await selectDevice(null) + } + await startCapture() + } + + const handleRetryAstra = async (): Promise => { + await handleSaveAstraConfig() + } + + const handleShowThemesFolder = async (): Promise => { + try { + await showThemesFolder() + } catch (error) { + showBanner({ + tone: 'error', + message: getErrorMessage(error, 'Could not open the themes folder.'), + actions: [], + }) + } } const handleRailWheel = (event: WheelEvent): void => { @@ -193,6 +244,11 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): : astraState.connectionState === 'error' ? 'Error' : 'Off' + const captureMessage = captureError ?? captureNotice + const astraErrorMessage = astraState.lastError ?? astraState.lastControlError + const canUseDefaultSource = captureMode === 'system' + ? selectedSystemSourceId !== defaultSystemSourceId + : selectedDeviceId !== null return (
@@ -243,7 +299,7 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): type="button" className="settings-chip" onClick={() => { - void showThemesFolder() + void handleShowThemesFolder() }} > Folder @@ -290,8 +346,28 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
- {astraState.lastError ? ( -
{astraState.lastError}
+ {astraErrorMessage ? ( + <> +
{astraErrorMessage}
+
+ + +
+ ) : null} @@ -312,19 +388,20 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): {visibleSystemSources.map((source) => ( + ))} + + + + {devices.map((device) => ( + ))} - {showInputDevices ? ( - - {devices.map((device) => ( - - ))} - - ) : null}
@@ -333,8 +410,43 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
- {captureError ? ( -
{captureError}
+ {captureMessage ? ( + <> +
+ {captureMessage} +
+
+ + {canUseDefaultSource ? ( + + ) : null} + {!captureError && captureNotice ? ( + + ) : null} +
+ ) : null} diff --git a/src/renderer/components/DialogApp.tsx b/src/renderer/components/DialogApp.tsx index f1e4d1f..51a930f 100644 --- a/src/renderer/components/DialogApp.tsx +++ b/src/renderer/components/DialogApp.tsx @@ -23,6 +23,11 @@ export default function DialogApp(): JSX.Element { const submit = useCallback((buttonIndex: number) => { if (!config) return + const isPrimaryPromptSubmit = config.type === 'prompt' && buttonIndex === (config.defaultId ?? 0) + if (isPrimaryPromptSubmit && !inputValue.trim()) { + return + } + const result: DialogResult = { buttonIndex } if (config.type === 'prompt') { result.value = inputValue @@ -47,6 +52,7 @@ export default function DialogApp(): JSX.Element { const primaryIndex = config.defaultId ?? 0 const cancelId = config.cancelId ?? config.buttons.length - 1 + const isPromptPrimaryDisabled = config.type === 'prompt' && !inputValue.trim() return (
@@ -63,6 +69,7 @@ export default function DialogApp(): JSX.Element { type="text" className="dialog-input" value={inputValue} + placeholder={config.placeholder} onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { @@ -88,6 +95,7 @@ export default function DialogApp(): JSX.Element { ].filter(Boolean).join(' ')} onClick={() => submit(i)} autoFocus={i === primaryIndex && config.type !== 'prompt'} + disabled={i === primaryIndex && isPromptPrimaryDisabled} > {label} diff --git a/src/renderer/components/Toolbar.tsx b/src/renderer/components/Toolbar.tsx index d746e74..3da7d2a 100644 --- a/src/renderer/components/Toolbar.tsx +++ b/src/renderer/components/Toolbar.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useRef, type JSX, type PointerEvent as ReactPointerEvent } from 'react' import { useSettingsStore } from '../stores/settingsStore' +import { useUiStore } from '../stores/uiStore' function SettingsIcon(): JSX.Element { return ( @@ -101,11 +102,37 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): const updateActiveProfile = useSettingsStore((s) => s.updateActiveProfile) const importProfileFromDialog = useSettingsStore((s) => s.importProfileFromDialog) const showProfilesFolder = useSettingsStore((s) => s.showProfilesFolder) + const showBanner = useUiStore((s) => s.showBanner) const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false) const [showReposition, setShowReposition] = useState(false) const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false) const profileButtonRef = useRef(null) + const showProfileErrorBanner = useCallback((error: unknown, fallback: string, includeOpenFolder = false) => { + const actions = includeOpenFolder + ? [{ + label: 'Open Folder', + onSelect: async () => { + try { + await showProfilesFolder() + } catch (folderError) { + showBanner({ + tone: 'error', + message: getErrorMessage(folderError, 'Could not open the profiles folder.'), + actions: [], + }) + } + }, + }] + : [] + + showBanner({ + tone: 'error', + message: getErrorMessage(error, fallback), + actions, + }) + }, [showBanner, showProfilesFolder]) + useEffect(() => { void window.electronAPI.isAlwaysOnTop().then(setIsAlwaysOnTop) const unsubscribe = window.electronAPI.onAlwaysOnTopChanged(setIsAlwaysOnTop) @@ -114,33 +141,33 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): const handleSaveNew = useCallback(async () => { setIsProfileMenuOpen(false) - const count = Object.keys(useSettingsStore.getState().profiles).length const result = await window.electronAPI.showDialog({ type: 'prompt', title: 'Save as New Profile', - message: 'Profile name', + message: 'Enter a name for the new profile.', buttons: ['Save', 'Cancel'], defaultId: 0, cancelId: 1, - defaultValue: `Profile ${count}`, + defaultValue: '', + placeholder: 'Profile name', }) if (result.buttonIndex !== 0 || !result.value?.trim()) return try { await saveProfile(result.value.trim()) } catch (error) { - window.alert(getErrorMessage(error, 'Could not save the profile.')) + showProfileErrorBanner(error, 'Could not save the profile.') } - }, [saveProfile]) + }, [saveProfile, showProfileErrorBanner]) const handleSaveOverwrite = useCallback(async () => { try { await updateActiveProfile() } catch (error) { - window.alert(getErrorMessage(error, 'Could not update the active profile.')) + showProfileErrorBanner(error, 'Could not update the active profile.') } finally { setIsProfileMenuOpen(false) } - }, [updateActiveProfile]) + }, [showProfileErrorBanner, updateActiveProfile]) const handleRenameActive = useCallback(async (id: string) => { const profile = useSettingsStore.getState().profiles[id] @@ -164,9 +191,9 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): try { await renameProfile(id, result.value.trim()) } catch (error) { - window.alert(getErrorMessage(error, 'Could not rename the profile.')) + showProfileErrorBanner(error, 'Could not rename the profile.') } - }, [renameProfile]) + }, [renameProfile, showProfileErrorBanner]) const handleDeleteActive = useCallback(async (id: string) => { const profile = useSettingsStore.getState().profiles[id] @@ -190,9 +217,9 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): try { await deleteProfile(id) } catch (error) { - window.alert(getErrorMessage(error, 'Could not delete the profile.')) + showProfileErrorBanner(error, 'Could not delete the profile.') } - }, [deleteProfile]) + }, [deleteProfile, showProfileErrorBanner]) const handleLoadProfile = useCallback(async (id: string) => { try { @@ -200,11 +227,11 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): await loadProfile(id) }) } catch (error) { - window.alert(getErrorMessage(error, 'Could not load the profile.')) + showProfileErrorBanner(error, 'Could not load the profile.', true) } finally { setIsProfileMenuOpen(false) } - }, [guardProfileTransition, loadProfile]) + }, [guardProfileTransition, loadProfile, showProfileErrorBanner]) const handleImportProfile = useCallback(async () => { try { @@ -212,21 +239,21 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps): await importProfileFromDialog() }) } catch (error) { - window.alert(getErrorMessage(error, 'Could not import the profile file.')) + showProfileErrorBanner(error, 'Could not import the profile file.', true) } finally { setIsProfileMenuOpen(false) } - }, [guardProfileTransition, importProfileFromDialog]) + }, [guardProfileTransition, importProfileFromDialog, showProfileErrorBanner]) const handleShowProfilesFolder = useCallback(async () => { try { await showProfilesFolder() } catch (error) { - window.alert(getErrorMessage(error, 'Could not open the profiles folder.')) + showProfileErrorBanner(error, 'Could not open the profiles folder.') } finally { setIsProfileMenuOpen(false) } - }, [showProfilesFolder]) + }, [showProfileErrorBanner, showProfilesFolder]) useEffect(() => { const offClosed = window.electronAPI.onProfileMenuClosed(() => { diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts index b83ab2f..8da0b0e 100644 --- a/src/renderer/env.d.ts +++ b/src/renderer/env.d.ts @@ -88,9 +88,6 @@ declare global { requestScopePopIn: (kind: ScopeKind) => void sendScopePopoutSettingsUpdate: (kind: ScopeKind, partial: unknown) => void onAlwaysOnTopChanged: (callback: (isOnTop: boolean) => void) => () => void - onToggleScope: (callback: (index: number) => void) => () => void - onToggleCapture: (callback: () => void) => () => void - onToggleSettings: (callback: () => void) => () => void onMainWindowBoundsChanged: (callback: (bounds: WindowBounds) => void) => () => void onAstraStateChanged: (callback: (state: AstraIntegrationState) => void) => () => void onMainCloseRequested: (callback: () => void) => () => void diff --git a/src/renderer/popouts/ScopePopoutWindow.tsx b/src/renderer/popouts/ScopePopoutWindow.tsx index 5d136be..8f0cefb 100644 --- a/src/renderer/popouts/ScopePopoutWindow.tsx +++ b/src/renderer/popouts/ScopePopoutWindow.tsx @@ -7,6 +7,7 @@ import ScopeModule from '../components/ScopeModule' import ScopeSettingsSection from '../components/ScopeSettingsSection' import WindowResizeOverlay from '../components/WindowResizeOverlay' import { usePerformanceStore } from '../stores/performanceStore' +import { useUiStore } from '../stores/uiStore' import { ScopePopoutDataSource } from './ScopePopoutDataSource' import { FrameScheduler } from '../visualizers/frameScheduler' @@ -66,9 +67,10 @@ const defaultTheme = resolveTheme(createDefaultTheme()) export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element { const [snapshot, setSnapshot] = useState | null>(null) const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false) - const [miniSettingsOpen, setMiniSettingsOpen] = useState(false) const prevMiniSettingsOpenRef = useRef(false) const frameTarget = usePerformanceStore((s) => s.frameTarget) + const miniSettingsOpen = useUiStore((s) => s.settingsOpen) + const setMiniSettingsOpen = useUiStore((s) => s.setSettingsOpen) const frameScheduler = useMemo(() => new FrameScheduler({ frameTarget }), []) const dataSource = useMemo(() => new ScopePopoutDataSource(scopeKind), [scopeKind]) @@ -226,7 +228,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)