diff --git a/src/components/pages/OverviewPage.tsx b/src/components/pages/OverviewPage.tsx new file mode 100644 index 0000000..57982d6 --- /dev/null +++ b/src/components/pages/OverviewPage.tsx @@ -0,0 +1,176 @@ +import { useState, useEffect, ReactNode } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { useAppContext } from '@/contexts/useAppContext' +import { Icon, IconName } from '@/components/icons' + +interface LocalReport { + game_id: string + unlocker: string + worked: boolean +} + +interface SystemInfo { + os_name: string + cpu_model: string + cpu_cores: number + gpu_name: string +} + +interface StatChipProps { + label: string + value: ReactNode + icon: IconName | string + variant?: string +} + +const StatChip = ({ label, value, icon, variant }: StatChipProps) => ( +
+ + {value} + {label} +
+) + +/** + * Overview page - the default landing view. Leads with the two library + * totals as hero numbers, then a Native/Proton composition bar for the + * Steam library, then secondary stats as compact chips, plus app info. + */ +const OverviewPage = () => { + const { games, epicGames, epicLoading, loadEpicGames, activityFeed } = useAppContext() + const [localReports, setLocalReports] = useState([]) + const [systemInfo, setSystemInfo] = useState(null) + + useEffect(() => { + invoke('get_local_reports') + .then(setLocalReports) + .catch(() => setLocalReports([])) + }, []) + + useEffect(() => { + invoke('get_system_info') + .then(setSystemInfo) + .catch(() => setSystemInfo(null)) + }, []) + + // Overview is the default landing page, so kick off the Epic scan here too + // otherwise the Epic count would show 0 until the user visits that tab. + useEffect(() => { + if (epicGames.length === 0 && !epicLoading) { + loadEpicGames() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const nativeCount = games.filter((g) => g.native).length + const protonCount = games.length - nativeCount + const creamCount = games.filter((g) => g.cream_installed).length + const smokeCount = games.filter((g) => g.smoke_installed).length + const nativePct = games.length ? (nativeCount / games.length) * 100 : 0 + const protonPct = games.length ? (protonCount / games.length) * 100 : 0 + + return ( +
+

Overview

+ +
+
+ {games.length} + Steam Games +
+
+
+ {epicLoading ? '—' : epicGames.length} + Epic Games +
+
+ + {games.length > 0 && ( +
+ Steam Library Composition +
+
+
+
+
+
+ + Native + {nativeCount} +
+
+ + Proton + {protonCount} +
+
+
+ )} + +
+ + + +
+ +
+
+

Recent Activity

+ + {activityFeed.length === 0 ? ( +

+ Nothing yet this session - install or uninstall something and it'll show up here. +

+ ) : ( +
    + {activityFeed.map((item) => ( +
  • + + {item.message} + + {new Date(item.timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + +
  • + ))} +
+ )} +
+ +
+

System

+ + {systemInfo ? ( +
+
+ Operating System + + {systemInfo.os_name} + +
+
+ Processor + + {systemInfo.cpu_model} + + {systemInfo.cpu_cores} threads +
+
+ Graphics + + {systemInfo.gpu_name} + +
+
+ ) : ( +

Reading system info...

+ )} +
+
+
+ ) +} + +export default OverviewPage diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx new file mode 100644 index 0000000..001d12b --- /dev/null +++ b/src/components/pages/SettingsPage.tsx @@ -0,0 +1,236 @@ +import { useState, useEffect, useCallback } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { open } from '@tauri-apps/plugin-dialog' +import { EntryList } from '@/components/common' +import { AnimatedCheckbox, Button } from '@/components/buttons' +import { useAppContext } from '@/contexts/useAppContext' +import { Config } from '@/types/Config' + +/** + * Settings page - Steam library paths, the compatibility reporting toggle, and a danger + * zone for resetting app data on disk. + */ +const SettingsPage = () => { + const [libraryPaths, setLibraryPaths] = useState([]) + const [reportingOptedIn, setReportingOptedIn] = useState(false) + const [isBusy, setIsBusy] = useState(false) + const [pathError, setPathError] = useState(null) + const [isResetting, setIsResetting] = useState(false) + const [isClearingCache, setIsClearingCache] = useState(false) + + const { loadGames, showToast } = useAppContext() + + const loadSettingsConfig = useCallback(async () => { + try { + const config = await invoke('load_config') + setLibraryPaths(config.custom_steam_paths) + setReportingOptedIn(config.reporting_opted_in) + } catch (error) { + console.error('Failed to load config:', error) + } + }, []) + + useEffect(() => { + loadSettingsConfig() + }, [loadSettingsConfig]) + + const addPath = async (path: string) => { + setPathError(null) + setIsBusy(true) + try { + const config = await invoke('add_custom_steam_path', { path }) + setLibraryPaths(config.custom_steam_paths) + loadGames() + showToast('Library path added - rescanning for games...', 'info') + } catch (error) { + setPathError(`${error}`) + } finally { + setIsBusy(false) + } + } + + const handleBrowse = async () => { + setPathError(null) + + let selected: string | null + try { + selected = await open({ directory: true, multiple: false, title: 'Select Steam Library Folder' }) + } catch (error) { + console.error('Failed to open folder picker:', error) + return + } + if (!selected) return + + addPath(selected) + } + + const handleRemovePath = async (path: string) => { + setIsBusy(true) + setPathError(null) + try { + const config = await invoke('remove_custom_steam_path', { path }) + setLibraryPaths(config.custom_steam_paths) + loadGames() + showToast('Library path removed - rescanning for games...', 'info') + } catch (error) { + setPathError(`${error}`) + } finally { + setIsBusy(false) + } + } + + const handleToggleReporting = async () => { + const next = !reportingOptedIn + setReportingOptedIn(next) + try { + await invoke('set_reporting_opt_in', { optedIn: next }) + showToast( + next ? 'Compatibility reporting enabled' : 'Compatibility reporting disabled', + 'info' + ) + } catch (error) { + setReportingOptedIn(!next) + showToast(`Failed to update reporting preference: ${error}`, 'error') + } + } + + const handleResetConfig = async () => { + if ( + !window.confirm( + 'Reset all settings to their defaults? This clears your custom Steam library paths and reporting preference, and brings back the startup disclaimer.' + ) + ) { + return + } + + setIsResetting(true) + try { + const config = await invoke('reset_config') + setLibraryPaths(config.custom_steam_paths) + setReportingOptedIn(config.reporting_opted_in) + loadGames() + showToast('Settings reset to defaults', 'success') + } catch (error) { + showToast(`Failed to reset settings: ${error}`, 'error') + } finally { + setIsResetting(false) + } + } + + const handleClearCache = async () => { + if ( + !window.confirm( + "Clear cached unlocker downloads? They'll be re-downloaded automatically the next time they're needed." + ) + ) { + return + } + + setIsClearingCache(true) + try { + await invoke('clear_caches') + showToast('Unlocker cache cleared', 'success') + } catch (error) { + showToast(`Failed to clear cache: ${error}`, 'error') + } finally { + setIsClearingCache(false) + } + } + + const handleOpenConfigFolder = async () => { + try { + await invoke('open_config_folder') + } catch (error) { + showToast(`Failed to open config folder: ${error}`, 'error') + } + } + + return ( +
+

Settings

+ +
+

Steam Library Paths

+

+ Add extra folders to scan for Steam games - useful if a library isn't auto-detected. +

+ + {pathError &&

{pathError}

} + + +
+ +
+

Compatibility Reporting

+

+ Anonymously report whether an unlocker worked for a game, and see how it's worked for + others before you install. +

+ + +
+ +
+

Danger Zone

+

+ These affect app data on disk - not your games or any unlockers already installed on + them. +

+ +
+
+ Reset all settings + + Restores defaults, including custom library paths and the startup disclaimer. + +
+ +
+ +
+
+ Clear unlocker cache + + Deletes downloaded CreamLinux/SmokeAPI/ScreamAPI/Koaloader files. + +
+ +
+ +
+
+ Open config folder + ~/.config/creamlinux +
+ +
+
+
+ ) +} + +export default SettingsPage