Show app version in sidebar, make the sidebar collapsible, and add a Light/Dark/Auto theme switch
- Expose the AeroFetch version over IPC (app:version → app.getVersion); the sidebar brand now shows "v<version>" beneath the name. - Sidebar can collapse to a 60px icon-only rail via a toggle button; nav items and the theme control fall back to icon + tooltip when collapsed. The state is persisted in localStorage so it survives restarts. Hint gains left/right placements for the collapsed-rail tooltips. - Replace the binary dark-mode Switch with an explicit 3-way Light / Dark / Auto segmented control (Auto follows the OS). Collapsed, it becomes a single button that cycles the three modes. - Bump version to 0.3.2. Verified in the browser UI preview: version label, collapse/expand, dark mode, and the theme switch all render correctly with no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aerofetch",
|
"name": "aerofetch",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"description": "A yt-dlp frontend for Windows",
|
"description": "A yt-dlp frontend for Windows",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "AeroFetch",
|
"author": "AeroFetch",
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ function createWindow(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerIpcHandlers(): void {
|
function registerIpcHandlers(): void {
|
||||||
|
ipcMain.handle(IpcChannels.appVersion, () => app.getVersion())
|
||||||
|
|
||||||
ipcMain.handle(IpcChannels.ytdlpVersion, () => getYtdlpVersion())
|
ipcMain.handle(IpcChannels.ytdlpVersion, () => getYtdlpVersion())
|
||||||
|
|
||||||
ipcMain.handle(IpcChannels.probe, (_e, url: string) => probeMedia(url))
|
ipcMain.handle(IpcChannels.probe, (_e, url: string) => probeMedia(url))
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import {
|
|||||||
|
|
||||||
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
||||||
const api = {
|
const api = {
|
||||||
|
/** AeroFetch's own version string (e.g. '0.3.1'). */
|
||||||
|
getAppVersion: (): Promise<string> => ipcRenderer.invoke(IpcChannels.appVersion),
|
||||||
|
|
||||||
getYtdlpVersion: (): Promise<YtdlpVersionResult> =>
|
getYtdlpVersion: (): Promise<YtdlpVersionResult> =>
|
||||||
ipcRenderer.invoke(IpcChannels.ytdlpVersion),
|
ipcRenderer.invoke(IpcChannels.ytdlpVersion),
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components'
|
import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components'
|
||||||
import { Sidebar, type TabValue } from './components/Sidebar'
|
import { Sidebar, type TabValue } from './components/Sidebar'
|
||||||
import { DownloadsView } from './components/DownloadsView'
|
import { DownloadsView } from './components/DownloadsView'
|
||||||
@@ -35,6 +35,24 @@ function App(): React.JSX.Element {
|
|||||||
const systemPrefersDark = useSystemTheme((s) => s.shouldUseDarkColors)
|
const systemPrefersDark = useSystemTheme((s) => s.shouldUseDarkColors)
|
||||||
const isDark = theme === 'system' ? systemPrefersDark : theme === 'dark'
|
const isDark = theme === 'system' ? systemPrefersDark : theme === 'dark'
|
||||||
const [tab, setTab] = useState<TabValue>('downloads')
|
const [tab, setTab] = useState<TabValue>('downloads')
|
||||||
|
|
||||||
|
// AeroFetch's own version, shown in the sidebar. Loaded once over IPC.
|
||||||
|
const [version, setVersion] = useState('')
|
||||||
|
useEffect(() => {
|
||||||
|
window.api?.getAppVersion?.().then(setVersion).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Sidebar collapse, persisted across launches in localStorage.
|
||||||
|
const [collapsed, setCollapsed] = useState(
|
||||||
|
() => localStorage.getItem('aerofetch.sidebarCollapsed') === '1'
|
||||||
|
)
|
||||||
|
function toggleCollapsed(): void {
|
||||||
|
setCollapsed((c) => {
|
||||||
|
const next = !c
|
||||||
|
localStorage.setItem('aerofetch.sidebarCollapsed', next ? '1' : '0')
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
// Gate on `loaded` so a returning user's real settings never get clobbered
|
// Gate on `loaded` so a returning user's real settings never get clobbered
|
||||||
// by a one-frame flash of the (default-false) onboarding state.
|
// by a one-frame flash of the (default-false) onboarding state.
|
||||||
const loaded = useSettings((s) => s.loaded)
|
const loaded = useSettings((s) => s.loaded)
|
||||||
@@ -60,9 +78,12 @@ function App(): React.JSX.Element {
|
|||||||
<Sidebar
|
<Sidebar
|
||||||
tab={tab}
|
tab={tab}
|
||||||
onTabChange={setTab}
|
onTabChange={setTab}
|
||||||
|
theme={theme}
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
followingSystem={theme === 'system'}
|
onSetTheme={(mode) => updateSettings({ theme: mode })}
|
||||||
onToggleTheme={() => updateSettings({ theme: isDark ? 'light' : 'dark' })}
|
version={version}
|
||||||
|
collapsed={collapsed}
|
||||||
|
onToggleCollapsed={toggleCollapsed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className={styles.content}>
|
<main className={styles.content}>
|
||||||
|
|||||||
@@ -30,13 +30,15 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
top: { bottom: 'calc(100% + 6px)' },
|
top: { bottom: 'calc(100% + 6px)' },
|
||||||
bottom: { top: 'calc(100% + 6px)' },
|
bottom: { top: 'calc(100% + 6px)' },
|
||||||
|
right: { left: 'calc(100% + 6px)', top: '50%', transform: 'translateY(-50%)' },
|
||||||
|
left: { right: 'calc(100% + 6px)', top: '50%', transform: 'translateY(-50%)' },
|
||||||
alignStart: { left: 0 },
|
alignStart: { left: 0 },
|
||||||
alignEnd: { right: 0 }
|
alignEnd: { right: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
interface HintProps {
|
interface HintProps {
|
||||||
label: string
|
label: string
|
||||||
placement?: 'top' | 'bottom'
|
placement?: 'top' | 'bottom' | 'left' | 'right'
|
||||||
align?: 'start' | 'end'
|
align?: 'start' | 'end'
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
@@ -56,8 +58,16 @@ export function Hint({
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
styles.bubble,
|
styles.bubble,
|
||||||
placement === 'bottom' ? styles.bottom : styles.top,
|
placement === 'bottom'
|
||||||
align === 'end' ? styles.alignEnd : styles.alignStart
|
? styles.bottom
|
||||||
|
: placement === 'right'
|
||||||
|
? styles.right
|
||||||
|
: placement === 'left'
|
||||||
|
? styles.left
|
||||||
|
: styles.top,
|
||||||
|
// start/end alignment only applies to vertical (top/bottom) placements
|
||||||
|
(placement === 'top' || placement === 'bottom') &&
|
||||||
|
(align === 'end' ? styles.alignEnd : styles.alignStart)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { Caption1, makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components'
|
||||||
Caption1,
|
|
||||||
Switch,
|
|
||||||
makeStyles,
|
|
||||||
mergeClasses,
|
|
||||||
tokens,
|
|
||||||
shorthands
|
|
||||||
} from '@fluentui/react-components'
|
|
||||||
import {
|
import {
|
||||||
ArrowDownloadFilled,
|
ArrowDownloadFilled,
|
||||||
ArrowDownloadRegular,
|
ArrowDownloadRegular,
|
||||||
@@ -13,8 +6,13 @@ import {
|
|||||||
LibraryRegular,
|
LibraryRegular,
|
||||||
SettingsRegular,
|
SettingsRegular,
|
||||||
WeatherMoonRegular,
|
WeatherMoonRegular,
|
||||||
WeatherSunnyRegular
|
WeatherSunnyRegular,
|
||||||
|
DesktopRegular,
|
||||||
|
PanelLeftContractRegular,
|
||||||
|
PanelLeftExpandRegular
|
||||||
} from '@fluentui/react-icons'
|
} from '@fluentui/react-icons'
|
||||||
|
import type { ThemeMode } from '@shared/ipc'
|
||||||
|
import { Hint } from './Hint'
|
||||||
|
|
||||||
export type TabValue = 'downloads' | 'library' | 'history' | 'settings'
|
export type TabValue = 'downloads' | 'library' | 'history' | 'settings'
|
||||||
|
|
||||||
@@ -27,13 +25,48 @@ const useStyles = makeStyles({
|
|||||||
gap: '4px',
|
gap: '4px',
|
||||||
padding: '16px 12px',
|
padding: '16px 12px',
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
borderRight: `1px solid ${tokens.colorNeutralStroke2}`
|
borderRight: `1px solid ${tokens.colorNeutralStroke2}`,
|
||||||
|
transition: 'width 0.15s ease'
|
||||||
|
},
|
||||||
|
rootCollapsed: {
|
||||||
|
width: '60px',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
topBar: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
paddingBottom: '2px'
|
||||||
|
},
|
||||||
|
topBarCollapsed: {
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
iconBtn: {
|
||||||
|
appearance: 'none',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: tokens.colorNeutralForeground3,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
fontSize: '18px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
...shorthands.borderRadius(tokens.borderRadiusMedium),
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||||
|
color: tokens.colorNeutralForeground2
|
||||||
|
}
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '11px',
|
gap: '11px',
|
||||||
padding: '6px 10px 14px'
|
padding: '0 10px 14px'
|
||||||
|
},
|
||||||
|
brandCollapsed: {
|
||||||
|
padding: '0 0 12px',
|
||||||
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
mark: {
|
mark: {
|
||||||
width: '36px',
|
width: '36px',
|
||||||
@@ -64,7 +97,8 @@ const useStyles = makeStyles({
|
|||||||
nav: {
|
nav: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '3px'
|
gap: '3px',
|
||||||
|
alignSelf: 'stretch'
|
||||||
},
|
},
|
||||||
navItem: {
|
navItem: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -84,6 +118,10 @@ const useStyles = makeStyles({
|
|||||||
backgroundColor: tokens.colorNeutralBackground1Hover
|
backgroundColor: tokens.colorNeutralBackground1Hover
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
navItemCollapsed: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '9px 0'
|
||||||
|
},
|
||||||
navItemActive: {
|
navItemActive: {
|
||||||
backgroundColor: tokens.colorBrandBackground2,
|
backgroundColor: tokens.colorBrandBackground2,
|
||||||
color: tokens.colorBrandForeground2,
|
color: tokens.colorBrandForeground2,
|
||||||
@@ -94,22 +132,46 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
navIcon: {
|
navIcon: {
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
flexShrink: 0
|
flexShrink: 0,
|
||||||
|
display: 'flex'
|
||||||
},
|
},
|
||||||
spacer: {
|
spacer: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
themeRow: {
|
// --- theme control (expanded): a 3-way Light / Dark / Auto segmented switch ---
|
||||||
|
themeGroup: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
width: '100%',
|
||||||
justifyContent: 'space-between',
|
border: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||||
padding: '8px 12px',
|
...shorthands.borderRadius(tokens.borderRadiusMedium),
|
||||||
color: tokens.colorNeutralForeground3
|
overflow: 'hidden'
|
||||||
},
|
},
|
||||||
themeLabel: {
|
themeSeg: {
|
||||||
|
flex: 1,
|
||||||
|
appearance: 'none',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: tokens.colorNeutralForeground2,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '9px'
|
justifyContent: 'center',
|
||||||
|
gap: '5px',
|
||||||
|
padding: '7px 4px',
|
||||||
|
fontSize: tokens.fontSizeBase200,
|
||||||
|
fontFamily: tokens.fontFamilyBase,
|
||||||
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1Hover
|
||||||
|
}
|
||||||
|
},
|
||||||
|
themeSegActive: {
|
||||||
|
backgroundColor: tokens.colorBrandBackground,
|
||||||
|
color: tokens.colorNeutralForegroundOnBrand,
|
||||||
|
fontWeight: tokens.fontWeightSemibold,
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: tokens.colorBrandBackgroundHover
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,67 +182,149 @@ const NAV: { value: TabValue; label: string; icon: React.JSX.Element }[] = [
|
|||||||
{ value: 'settings', label: 'Settings', icon: <SettingsRegular /> }
|
{ value: 'settings', label: 'Settings', icon: <SettingsRegular /> }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const THEMES: { value: ThemeMode; label: string; icon: React.JSX.Element }[] = [
|
||||||
|
{ value: 'light', label: 'Light', icon: <WeatherSunnyRegular /> },
|
||||||
|
{ value: 'dark', label: 'Dark', icon: <WeatherMoonRegular /> },
|
||||||
|
{ value: 'system', label: 'Auto', icon: <DesktopRegular /> }
|
||||||
|
]
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
tab: TabValue
|
tab: TabValue
|
||||||
onTabChange: (t: TabValue) => void
|
onTabChange: (t: TabValue) => void
|
||||||
|
/** the current theme preference: explicit light/dark, or 'system' to follow the OS */
|
||||||
|
theme: ThemeMode
|
||||||
|
/** whether the resolved theme is currently dark (drives the collapsed toggle icon) */
|
||||||
isDark: boolean
|
isDark: boolean
|
||||||
/** true when theme is 'system' — the quick toggle still works (it sets an explicit mode) */
|
onSetTheme: (mode: ThemeMode) => void
|
||||||
followingSystem: boolean
|
/** AeroFetch version string, e.g. '0.3.2' ('' until loaded) */
|
||||||
onToggleTheme: () => void
|
version: string
|
||||||
|
collapsed: boolean
|
||||||
|
onToggleCollapsed: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
tab,
|
tab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
|
theme,
|
||||||
isDark,
|
isDark,
|
||||||
followingSystem,
|
onSetTheme,
|
||||||
onToggleTheme
|
version,
|
||||||
|
collapsed,
|
||||||
|
onToggleCollapsed
|
||||||
}: SidebarProps): React.JSX.Element {
|
}: SidebarProps): React.JSX.Element {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
|
// Collapsed view shows one button that cycles Light → Dark → Auto.
|
||||||
|
const order: ThemeMode[] = ['light', 'dark', 'system']
|
||||||
|
function cycleTheme(): void {
|
||||||
|
onSetTheme(order[(order.indexOf(theme) + 1) % order.length])
|
||||||
|
}
|
||||||
|
const themeIcon =
|
||||||
|
theme === 'system' ? (
|
||||||
|
<DesktopRegular fontSize={18} />
|
||||||
|
) : isDark ? (
|
||||||
|
<WeatherMoonRegular fontSize={18} />
|
||||||
|
) : (
|
||||||
|
<WeatherSunnyRegular fontSize={18} />
|
||||||
|
)
|
||||||
|
const themeLabel = theme === 'system' ? 'Auto (system)' : isDark ? 'Dark' : 'Light'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.root}>
|
<nav className={mergeClasses(styles.root, collapsed && styles.rootCollapsed)}>
|
||||||
<div className={styles.brand}>
|
<div className={mergeClasses(styles.topBar, collapsed && styles.topBarCollapsed)}>
|
||||||
|
<Hint label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'} placement="right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.iconBtn}
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
|
aria-pressed={collapsed}
|
||||||
|
>
|
||||||
|
{collapsed ? <PanelLeftExpandRegular /> : <PanelLeftContractRegular />}
|
||||||
|
</button>
|
||||||
|
</Hint>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={mergeClasses(styles.brand, collapsed && styles.brandCollapsed)}>
|
||||||
<div className={styles.mark}>
|
<div className={styles.mark}>
|
||||||
<ArrowDownloadFilled />
|
<ArrowDownloadFilled />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.brandText}>
|
{!collapsed && (
|
||||||
<span className={styles.brandName}>AeroFetch</span>
|
<div className={styles.brandText}>
|
||||||
<Caption1 className={styles.caption}>yt-dlp frontend</Caption1>
|
<span className={styles.brandName}>AeroFetch</span>
|
||||||
</div>
|
<Caption1 className={styles.caption}>{version ? `v${version}` : 'yt-dlp frontend'}</Caption1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.nav}>
|
<div className={styles.nav}>
|
||||||
{NAV.map((n) => {
|
{NAV.map((n) => {
|
||||||
const active = tab === n.value
|
const active = tab === n.value
|
||||||
return (
|
const btn = (
|
||||||
<button
|
<button
|
||||||
key={n.value}
|
key={n.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={mergeClasses(styles.navItem, active && styles.navItemActive)}
|
className={mergeClasses(
|
||||||
|
styles.navItem,
|
||||||
|
collapsed && styles.navItemCollapsed,
|
||||||
|
active && styles.navItemActive
|
||||||
|
)}
|
||||||
style={
|
style={
|
||||||
active
|
active && !collapsed
|
||||||
? { boxShadow: `inset 3px 0 0 0 ${tokens.colorCompoundBrandStroke}` }
|
? { boxShadow: `inset 3px 0 0 0 ${tokens.colorCompoundBrandStroke}` }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onClick={() => onTabChange(n.value)}
|
onClick={() => onTabChange(n.value)}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
|
aria-label={n.label}
|
||||||
>
|
>
|
||||||
<span className={styles.navIcon}>{n.icon}</span>
|
<span className={styles.navIcon}>{n.icon}</span>
|
||||||
{n.label}
|
{!collapsed && n.label}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
return collapsed ? (
|
||||||
|
<Hint key={n.value} label={n.label} placement="right">
|
||||||
|
{btn}
|
||||||
|
</Hint>
|
||||||
|
) : (
|
||||||
|
btn
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
|
|
||||||
<div className={styles.themeRow}>
|
{collapsed ? (
|
||||||
<span className={styles.themeLabel}>
|
<Hint label={`Theme: ${themeLabel}`} placement="right">
|
||||||
{isDark ? <WeatherMoonRegular fontSize={18} /> : <WeatherSunnyRegular fontSize={18} />}
|
<button
|
||||||
{(isDark ? 'Dark' : 'Light') + (followingSystem ? ' · Auto' : '')}
|
type="button"
|
||||||
</span>
|
className={styles.iconBtn}
|
||||||
<Switch checked={isDark} onChange={onToggleTheme} aria-label="Toggle dark mode" />
|
onClick={cycleTheme}
|
||||||
</div>
|
aria-label={`Theme: ${themeLabel}. Click to change.`}
|
||||||
|
>
|
||||||
|
{themeIcon}
|
||||||
|
</button>
|
||||||
|
</Hint>
|
||||||
|
) : (
|
||||||
|
<div className={styles.themeGroup} role="radiogroup" aria-label="Theme">
|
||||||
|
{THEMES.map((t) => {
|
||||||
|
const on = theme === t.value
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.value}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={on}
|
||||||
|
className={mergeClasses(styles.themeSeg, on && styles.themeSegActive)}
|
||||||
|
onClick={() => onSetTheme(t.value)}
|
||||||
|
>
|
||||||
|
<span className={styles.navIcon}>{t.icon}</span>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ if (import.meta.env.DEV && !window.api) {
|
|||||||
]
|
]
|
||||||
|
|
||||||
window.api = {
|
window.api = {
|
||||||
|
getAppVersion: async () => '0.3.2-preview',
|
||||||
getYtdlpVersion: async () => ({ ok: true, version: '2025.06.01 (UI preview mock)' }),
|
getYtdlpVersion: async () => ({ ok: true, version: '2025.06.01 (UI preview mock)' }),
|
||||||
probe: async (url: string) => {
|
probe: async (url: string) => {
|
||||||
await new Promise((r) => setTimeout(r, 700)) // simulate network latency
|
await new Promise((r) => setTimeout(r, 700)) // simulate network latency
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const IpcChannels = {
|
export const IpcChannels = {
|
||||||
|
/** the AeroFetch app version (package.json / app.getVersion) */
|
||||||
|
appVersion: 'app:version',
|
||||||
ytdlpVersion: 'ytdlp:version',
|
ytdlpVersion: 'ytdlp:version',
|
||||||
probe: 'media:probe',
|
probe: 'media:probe',
|
||||||
downloadStart: 'download:start',
|
downloadStart: 'download:start',
|
||||||
|
|||||||
Reference in New Issue
Block a user