From d6684ccb212857a125a9230e08c343c9f48197ec Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 5 Sep 2025 12:56:55 -0700 Subject: [PATCH] UI v2 (#103) * wip ui * pretty much fully new settings page * proper dark/light mode * slight style tweak for sidebar * add about --- bun.lockb | Bin 68421 -> 69352 bytes package.json | 4 +- src-tauri/tauri.conf.json | 8 +- src/App.tsx | 64 ++- src/components/Sidebar.tsx | 105 +++++ src/components/footer/Footer.tsx | 2 +- src/components/icons/HandyHand.tsx | 24 ++ src/components/icons/HandyTextLogo.tsx | 3 + src/components/settings/AboutSettings.tsx | 70 ++++ src/components/settings/AdvancedSettings.tsx | 21 + src/components/settings/AudioFeedback.tsx | 25 +- src/components/settings/DebugSettings.tsx | 15 + src/components/settings/GeneralSettings.tsx | 31 ++ src/components/settings/HistorySettings.tsx | 12 + .../settings/OutputDeviceSelector.tsx | 111 ++--- src/components/settings/Settings.tsx | 73 ---- src/components/settings/index.ts | 9 +- src/components/ui/Button.tsx | 2 +- src/components/ui/ResetButton.tsx | 6 +- src/components/ui/SettingContainer.tsx | 30 +- src/hooks/useSettings.ts | 390 ++---------------- src/stores/settingsStore.ts | 300 ++++++++++++++ 22 files changed, 783 insertions(+), 522 deletions(-) create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/icons/HandyHand.tsx create mode 100644 src/components/settings/AboutSettings.tsx create mode 100644 src/components/settings/AdvancedSettings.tsx create mode 100644 src/components/settings/DebugSettings.tsx create mode 100644 src/components/settings/GeneralSettings.tsx create mode 100644 src/components/settings/HistorySettings.tsx delete mode 100644 src/components/settings/Settings.tsx create mode 100644 src/stores/settingsStore.ts diff --git a/bun.lockb b/bun.lockb index 8fbc20a54ef16ca3b148932744f6794ac775dde1..2e86f7efd579d8ba3894853bbc2c0cf91d376220 100755 GIT binary patch delta 11688 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 BtOEc5 delta 11050 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 { + const ActiveComponent = + SECTIONS_CONFIG[section]?.component || SECTIONS_CONFIG.general.component; + return ; +}; function App() { const [showOnboarding, setShowOnboarding] = useState(null); + const [currentSection, setCurrentSection] = + useState("general"); + const { settings, updateSetting } = useSettings(); useEffect(() => { checkOnboardingStatus(); }, []); + // Handle keyboard shortcuts for debug mode toggle + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Check for Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS) + const isDebugShortcut = + event.shiftKey && + event.key.toLowerCase() === "d" && + (event.ctrlKey || event.metaKey); + + if (isDebugShortcut) { + event.preventDefault(); + const currentDebugMode = settings?.debug_mode ?? false; + updateSetting("debug_mode", !currentDebugMode); + } + }; + + // Add event listener when component mounts + document.addEventListener("keydown", handleKeyDown); + + // Cleanup event listener when component unmounts + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [settings?.debug_mode, updateSetting]); + const checkOnboardingStatus = async () => { try { // Always check if they have any models available @@ -33,23 +68,36 @@ function App() { if (showOnboarding) { return ( -
-
+
+
+
); } return ( -
+
-
- - - + {/* Main content area that takes remaining space */} +
+ + {/* Scrollable content area */} +
+
+
+ + {renderSettingsContent(currentSection)} +
+
+
+ {/* Fixed footer at bottom */}
); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..5ae0d59 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { Cog, FlaskConical, History, Info } from "lucide-react"; +import HandyTextLogo from "./icons/HandyTextLogo"; +import HandyHand from "./icons/HandyHand"; +import { useSettings } from "../hooks/useSettings"; +import { + GeneralSettings, + AdvancedSettings, + HistorySettings, + DebugSettings, + AboutSettings, +} from "./settings"; + +export type SidebarSection = keyof typeof SECTIONS_CONFIG; + +interface IconProps { + width?: number | string; + height?: number | string; + size?: number | string; + className?: string; + [key: string]: any; +} + +interface SectionConfig { + label: string; + icon: React.ComponentType; + component: React.ComponentType; + enabled: (settings: any) => boolean; +} + +export const SECTIONS_CONFIG = { + general: { + label: "General", + icon: HandyHand, + component: GeneralSettings, + enabled: () => true, + }, + advanced: { + label: "Advanced", + icon: Cog, + component: AdvancedSettings, + enabled: () => true, + }, + // history: { + // label: "History", + // icon: History, + // component: HistorySettings, + // enabled: () => true, + // }, + debug: { + label: "Debug", + icon: FlaskConical, + component: DebugSettings, + enabled: (settings) => settings?.debug_mode ?? false, + }, + about: { + label: "About", + icon: Info, + component: AboutSettings, + enabled: () => true, + }, +} as const satisfies Record; + +interface SidebarProps { + activeSection: SidebarSection; + onSectionChange: (section: SidebarSection) => void; +} + +export const Sidebar: React.FC = ({ + activeSection, + onSectionChange, +}) => { + const { settings } = useSettings(); + + const availableSections = Object.entries(SECTIONS_CONFIG) + .filter(([_, config]) => config.enabled(settings)) + .map(([id, config]) => ({ id: id as SidebarSection, ...config })); + + return ( +
+ +
+ {availableSections.map((section) => { + const Icon = section.icon; + const isActive = activeSection === section.id; + + return ( +
onSectionChange(section.id)} + > + +

{section.label}

+
+ ); + })} +
+
+ ); +}; diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 3022dfd..b901a04 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -22,7 +22,7 @@ const Footer: React.FC = () => { }, []); return ( -
+
diff --git a/src/components/icons/HandyHand.tsx b/src/components/icons/HandyHand.tsx new file mode 100644 index 0000000..fc115b6 --- /dev/null +++ b/src/components/icons/HandyHand.tsx @@ -0,0 +1,24 @@ +const HandyHand = ({ + width, + height, +}: { + width?: number | string; + height?: number | string; +}) => ( + + + +); + +export default HandyHand; diff --git a/src/components/icons/HandyTextLogo.tsx b/src/components/icons/HandyTextLogo.tsx index fc41bd5..008f89b 100644 --- a/src/components/icons/HandyTextLogo.tsx +++ b/src/components/icons/HandyTextLogo.tsx @@ -3,14 +3,17 @@ import React from "react"; const HandyTextLogo = ({ width, height, + className, }: { width?: number; height?: number; + className?: string; }) => { return ( { + const [version, setVersion] = useState(""); + + useEffect(() => { + const fetchVersion = async () => { + try { + const appVersion = await getVersion(); + setVersion(appVersion); + } catch (error) { + console.error("Failed to get app version:", error); + setVersion("0.1.2"); + } + }; + + fetchVersion(); + }, []); + + const handleDonateClick = async () => { + try { + await openUrl("https://handy.computer/donate"); + } catch (error) { + console.error("Failed to open donate link:", error); + } + }; + + return ( +
+ + + v{version} + + + + + + + + + +
+ Handy uses Whisper.cpp for fast, local speech-to-text processing. + Thanks to the amazing work by Georgi Gerganov and contributors. +
+
+
+
+ ); +}; diff --git a/src/components/settings/AdvancedSettings.tsx b/src/components/settings/AdvancedSettings.tsx new file mode 100644 index 0000000..2ca6c81 --- /dev/null +++ b/src/components/settings/AdvancedSettings.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ShowOverlay } from "./ShowOverlay"; +import { TranslateToEnglish } from "./TranslateToEnglish"; +import { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout"; +import { CustomWords } from "./CustomWords"; +import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone"; +import { SettingsGroup } from "../ui/SettingsGroup"; + +export const AdvancedSettings: React.FC = () => { + return ( +
+ + + + + + + +
+ ); +}; diff --git a/src/components/settings/AudioFeedback.tsx b/src/components/settings/AudioFeedback.tsx index 36524c1..7aeb53d 100644 --- a/src/components/settings/AudioFeedback.tsx +++ b/src/components/settings/AudioFeedback.tsx @@ -15,15 +15,16 @@ export const AudioFeedback: React.FC = React.memo(({ const audioFeedbackEnabled = getSetting("audio_feedback") || false; - return ( - updateSetting("audio_feedback", enabled)} - isUpdating={isUpdating("audio_feedback")} - label="Audio Feedback" - description="Play sound when recording starts and stops" - descriptionMode={descriptionMode} - grouped={grouped} - /> - ); -}); + return ( + updateSetting("audio_feedback", enabled)} + isUpdating={isUpdating("audio_feedback")} + label="Audio Feedback" + description="Play sound when recording starts and stops" + descriptionMode={descriptionMode} + grouped={grouped} + /> + ); + }, +); diff --git a/src/components/settings/DebugSettings.tsx b/src/components/settings/DebugSettings.tsx new file mode 100644 index 0000000..55ae57d --- /dev/null +++ b/src/components/settings/DebugSettings.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold"; +import { AppDataDirectory } from "./AppDataDirectory"; +import { SettingsGroup } from "../ui/SettingsGroup"; + +export const DebugSettings: React.FC = () => { + return ( +
+ + + + +
+ ); +}; diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx new file mode 100644 index 0000000..5ff6af6 --- /dev/null +++ b/src/components/settings/GeneralSettings.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { MicrophoneSelector } from "./MicrophoneSelector"; +import { LanguageSelector } from "./LanguageSelector"; +import { HandyShortcut } from "./HandyShortcut"; +import { SettingsGroup } from "../ui/SettingsGroup"; +import { OutputDeviceSelector } from "./OutputDeviceSelector"; +import { PushToTalk } from "./PushToTalk"; +import { AudioFeedback } from "./AudioFeedback"; +import { useSettings } from "../../hooks/useSettings"; + +export const GeneralSettings: React.FC = () => { + const { audioFeedbackEnabled } = useSettings(); + return ( +
+ + + + + + + + + + +
+ ); +}; diff --git a/src/components/settings/HistorySettings.tsx b/src/components/settings/HistorySettings.tsx new file mode 100644 index 0000000..87fbad0 --- /dev/null +++ b/src/components/settings/HistorySettings.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { SettingsGroup } from "../ui/SettingsGroup"; + +export const HistorySettings: React.FC = () => { + return ( +
+ + <> + +
+ ); +}; diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index b0efd70..9613fd5 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -3,62 +3,79 @@ import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; +import { AudioDevice } from "../../lib/types"; interface OutputDeviceSelectorProps { descriptionMode?: "inline" | "tooltip"; grouped?: boolean; + disabled?: boolean; } -export const OutputDeviceSelector: React.FC = React.memo(({ - descriptionMode = "tooltip", - grouped = false, -}) => { - const { - getSetting, - updateSetting, - resetSetting, - isUpdating, - isLoading, - outputDevices, - refreshOutputDevices, - } = useSettings(); +export const OutputDeviceSelector: React.FC = + React.memo( + ({ descriptionMode = "tooltip", grouped = false, disabled = false }) => { + const { + getSetting, + updateSetting, + resetSetting, + isUpdating, + isLoading, + outputDevices, + refreshOutputDevices, + } = useSettings(); - const selectedOutputDevice = getSetting("selected_output_device") === "default" ? "Default" : getSetting("selected_output_device") || "Default"; + const selectedOutputDevice = + getSetting("selected_output_device") === "default" + ? "Default" + : getSetting("selected_output_device") || "Default"; - const handleOutputDeviceSelect = async (deviceName: string) => { - await updateSetting("selected_output_device", deviceName); - }; + const handleOutputDeviceSelect = async (deviceName: string) => { + await updateSetting("selected_output_device", deviceName); + }; - const handleReset = async () => { - await resetSetting("selected_output_device"); - }; + const handleReset = async () => { + await resetSetting("selected_output_device"); + }; - const outputDeviceOptions = outputDevices.map(device => ({ - value: device.name, - label: device.name - })); + const outputDeviceOptions = outputDevices.map((device: AudioDevice) => ({ + value: device.name, + label: device.name, + })); - return ( - -
- - -
-
+ return ( + +
+ + +
+
+ ); + }, ); -}); diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx deleted file mode 100644 index db7bb4a..0000000 --- a/src/components/settings/Settings.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect } from "react"; -import { MicrophoneSelector } from "./MicrophoneSelector"; -import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone"; -import { PushToTalk } from "./PushToTalk"; -import { AudioFeedback } from "./AudioFeedback"; -import { OutputDeviceSelector } from "./OutputDeviceSelector"; -import { ShowOverlay } from "./ShowOverlay"; -import { HandyShortcut } from "./HandyShortcut"; -import { TranslateToEnglish } from "./TranslateToEnglish"; -import { LanguageSelector } from "./LanguageSelector"; -import { CustomWords } from "./CustomWords"; -import { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout"; -import { SettingsGroup } from "../ui/SettingsGroup"; -import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold"; -import { AppDataDirectory } from "./AppDataDirectory"; -import { useSettings } from "../../hooks/useSettings"; - -export const Settings: React.FC = () => { - const { settings, updateSetting } = useSettings(); - - // Handle keyboard shortcuts for debug mode toggle - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Check for Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS) - const isDebugShortcut = - event.shiftKey && - event.key.toLowerCase() === "d" && - (event.ctrlKey || event.metaKey); - - if (isDebugShortcut) { - event.preventDefault(); - const currentDebugMode = settings?.debug_mode ?? false; - updateSetting("debug_mode", !currentDebugMode); - } - }; - - // Add event listener when component mounts - document.addEventListener("keydown", handleKeyDown); - - // Cleanup event listener when component unmounts - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [settings?.debug_mode, updateSetting]); - - return ( -
- - - - - - - - - - - - - - - - - - {settings?.debug_mode && ( - - - - - )} -
- ); -}; diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index ec094de..19c7f69 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1,4 +1,11 @@ -export { Settings } from "./Settings"; +// Settings section components +export { GeneralSettings } from "./GeneralSettings"; +export { AdvancedSettings } from "./AdvancedSettings"; +export { DebugSettings } from "./DebugSettings"; +export { HistorySettings } from "./HistorySettings"; +export { AboutSettings } from "./AboutSettings"; + +// Individual setting components export { MicrophoneSelector } from "./MicrophoneSelector"; export { OutputDeviceSelector } from "./OutputDeviceSelector"; export { AlwaysOnMicrophone } from "./AlwaysOnMicrophone"; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 13462ca..abba817 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -17,7 +17,7 @@ export const Button: React.FC = ({ const variantClasses = { primary: - "text-white bg-background-ui hover:bg-background-ui/90 focus:ring-1 focus:ring-background-ui", + "text-white bg-background-ui hover:bg-background-ui/80 focus:ring-1 focus:ring-background-ui", secondary: "bg-mid-gray/10 hover:bg-background-ui/30 focus:outline-none", danger: "text-white bg-red-600 hover:bg-red-700 focus:ring-1 focus:ring-red-500", diff --git a/src/components/ui/ResetButton.tsx b/src/components/ui/ResetButton.tsx index 4f8a7d3..05328f2 100644 --- a/src/components/ui/ResetButton.tsx +++ b/src/components/ui/ResetButton.tsx @@ -10,7 +10,11 @@ interface ResetButtonProps { export const ResetButton: React.FC = React.memo( ({ onClick, disabled = false, className = "" }) => (