From 271862482898bed2c60d4972e97804578b5832d4 Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 24 Jun 2026 06:43:44 -0400 Subject: [PATCH] Show app version in sidebar, make the sidebar collapsible, and add a Light/Dark/Auto theme switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expose the AeroFetch version over IPC (app:version → app.getVersion); the sidebar brand now shows "v" 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 --- package.json | 2 +- src/main/index.ts | 2 + src/preload/index.ts | 3 + src/renderer/src/App.tsx | 27 ++- src/renderer/src/components/Hint.tsx | 16 +- src/renderer/src/components/Sidebar.tsx | 228 +++++++++++++++++++----- src/renderer/src/main.tsx | 1 + src/shared/ipc.ts | 2 + 8 files changed, 232 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index c1a88d3..3f93f07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aerofetch", - "version": "0.3.1", + "version": "0.3.2", "description": "A yt-dlp frontend for Windows", "main": "./out/main/index.js", "author": "AeroFetch", diff --git a/src/main/index.ts b/src/main/index.ts index 8e7b2f5..f30e46e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -151,6 +151,8 @@ function createWindow(): void { } function registerIpcHandlers(): void { + ipcMain.handle(IpcChannels.appVersion, () => app.getVersion()) + ipcMain.handle(IpcChannels.ytdlpVersion, () => getYtdlpVersion()) ipcMain.handle(IpcChannels.probe, (_e, url: string) => probeMedia(url)) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9628b65..aa93a80 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -28,6 +28,9 @@ import { // The surface exposed to the renderer. Keep this thin: it only forwards to IPC. const api = { + /** AeroFetch's own version string (e.g. '0.3.1'). */ + getAppVersion: (): Promise => ipcRenderer.invoke(IpcChannels.appVersion), + getYtdlpVersion: (): Promise => ipcRenderer.invoke(IpcChannels.ytdlpVersion), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1645e4b..0be8f38 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { FluentProvider, makeStyles, tokens } from '@fluentui/react-components' import { Sidebar, type TabValue } from './components/Sidebar' import { DownloadsView } from './components/DownloadsView' @@ -35,6 +35,24 @@ function App(): React.JSX.Element { const systemPrefersDark = useSystemTheme((s) => s.shouldUseDarkColors) const isDark = theme === 'system' ? systemPrefersDark : theme === 'dark' const [tab, setTab] = useState('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 // by a one-frame flash of the (default-false) onboarding state. const loaded = useSettings((s) => s.loaded) @@ -60,9 +78,12 @@ function App(): React.JSX.Element { updateSettings({ theme: isDark ? 'light' : 'dark' })} + onSetTheme={(mode) => updateSettings({ theme: mode })} + version={version} + collapsed={collapsed} + onToggleCollapsed={toggleCollapsed} />
diff --git a/src/renderer/src/components/Hint.tsx b/src/renderer/src/components/Hint.tsx index 57f99f3..767d084 100644 --- a/src/renderer/src/components/Hint.tsx +++ b/src/renderer/src/components/Hint.tsx @@ -30,13 +30,15 @@ const useStyles = makeStyles({ }, top: { bottom: '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 }, alignEnd: { right: 0 } }) interface HintProps { label: string - placement?: 'top' | 'bottom' + placement?: 'top' | 'bottom' | 'left' | 'right' align?: 'start' | 'end' children: React.ReactNode } @@ -56,8 +58,16 @@ export function Hint({ aria-hidden className={mergeClasses( styles.bubble, - placement === 'bottom' ? styles.bottom : styles.top, - align === 'end' ? styles.alignEnd : styles.alignStart + placement === 'bottom' + ? 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} diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 9473a62..550d66f 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -1,11 +1,4 @@ -import { - Caption1, - Switch, - makeStyles, - mergeClasses, - tokens, - shorthands -} from '@fluentui/react-components' +import { Caption1, makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components' import { ArrowDownloadFilled, ArrowDownloadRegular, @@ -13,8 +6,13 @@ import { LibraryRegular, SettingsRegular, WeatherMoonRegular, - WeatherSunnyRegular + WeatherSunnyRegular, + DesktopRegular, + PanelLeftContractRegular, + PanelLeftExpandRegular } from '@fluentui/react-icons' +import type { ThemeMode } from '@shared/ipc' +import { Hint } from './Hint' export type TabValue = 'downloads' | 'library' | 'history' | 'settings' @@ -27,13 +25,48 @@ const useStyles = makeStyles({ gap: '4px', padding: '16px 12px', 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: { display: 'flex', alignItems: 'center', gap: '11px', - padding: '6px 10px 14px' + padding: '0 10px 14px' + }, + brandCollapsed: { + padding: '0 0 12px', + justifyContent: 'center' }, mark: { width: '36px', @@ -64,7 +97,8 @@ const useStyles = makeStyles({ nav: { display: 'flex', flexDirection: 'column', - gap: '3px' + gap: '3px', + alignSelf: 'stretch' }, navItem: { display: 'flex', @@ -84,6 +118,10 @@ const useStyles = makeStyles({ backgroundColor: tokens.colorNeutralBackground1Hover } }, + navItemCollapsed: { + justifyContent: 'center', + padding: '9px 0' + }, navItemActive: { backgroundColor: tokens.colorBrandBackground2, color: tokens.colorBrandForeground2, @@ -94,22 +132,46 @@ const useStyles = makeStyles({ }, navIcon: { fontSize: '18px', - flexShrink: 0 + flexShrink: 0, + display: 'flex' }, spacer: { flexGrow: 1 }, - themeRow: { + // --- theme control (expanded): a 3-way Light / Dark / Auto segmented switch --- + themeGroup: { + alignSelf: 'stretch', display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '8px 12px', - color: tokens.colorNeutralForeground3 + width: '100%', + border: `1px solid ${tokens.colorNeutralStroke1}`, + ...shorthands.borderRadius(tokens.borderRadiusMedium), + overflow: 'hidden' }, - themeLabel: { + themeSeg: { + flex: 1, + appearance: 'none', + border: 'none', + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground2, display: 'flex', 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: } ] +const THEMES: { value: ThemeMode; label: string; icon: React.JSX.Element }[] = [ + { value: 'light', label: 'Light', icon: }, + { value: 'dark', label: 'Dark', icon: }, + { value: 'system', label: 'Auto', icon: } +] + interface SidebarProps { tab: TabValue 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 - /** true when theme is 'system' — the quick toggle still works (it sets an explicit mode) */ - followingSystem: boolean - onToggleTheme: () => void + onSetTheme: (mode: ThemeMode) => void + /** AeroFetch version string, e.g. '0.3.2' ('' until loaded) */ + version: string + collapsed: boolean + onToggleCollapsed: () => void } export function Sidebar({ tab, onTabChange, + theme, isDark, - followingSystem, - onToggleTheme + onSetTheme, + version, + collapsed, + onToggleCollapsed }: SidebarProps): React.JSX.Element { 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' ? ( + + ) : isDark ? ( + + ) : ( + + ) + const themeLabel = theme === 'system' ? 'Auto (system)' : isDark ? 'Dark' : 'Light' + return ( -