diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index 4071da6..30e8908 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -856,6 +856,11 @@ fn ui_operation_from_peer(operation: ActiveOperationKind) -> UiOperationKind { } } +#[tauri::command] +fn game_directory_exists(path: String) -> bool { + PathBuf::from(path).is_dir() +} + #[tauri::command] async fn update_game_directory(app_handle: tauri::AppHandle, path: String) -> tauri::Result<()> { log::info!("update_game_directory: {path}"); @@ -1803,6 +1808,7 @@ pub fn run() { install_game, run_game, start_server, + game_directory_exists, update_game_directory, update_game, uninstall_game, diff --git a/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx b/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx index 0097825..0c15609 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx @@ -7,11 +7,7 @@ interface Props { export const NoDirectoryState = ({ onChooseDirectory }: Props) => (
-

Pick a game directory

-

- SoftLAN scans the folder you point it at for installable game bundles - and tracks what your peers on the LAN have available. -

+

Please select a game folder

diff --git a/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts b/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts index 80eba16..f65c8a2 100644 --- a/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts +++ b/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts @@ -11,6 +11,7 @@ import { GAME_DIR_KEY, SETTINGS_FILE, SETTINGS_FILE_OPTIONS } from '../lib/store */ export const useGameDirectory = () => { const [gameDir, setGameDir] = useState(''); + const [gameDirExists, setGameDirExists] = useState(false); useEffect(() => { let cancelled = false; @@ -30,7 +31,11 @@ export const useGameDirectory = () => { }, []); useEffect(() => { - if (!gameDir) return; + if (!gameDir.trim()) { + setGameDirExists(false); + return; + } + let cancelled = false; const sync = async () => { try { const store = await load(SETTINGS_FILE, SETTINGS_FILE_OPTIONS); @@ -38,19 +43,48 @@ export const useGameDirectory = () => { } catch (err) { console.error('Failed to persist game directory:', err); } + + let exists = false; + try { + exists = await invoke('game_directory_exists', { path: gameDir }); + } catch (err) { + console.error('Failed to validate game directory:', err); + } + if (cancelled) return; + setGameDirExists(exists); + if (!exists) return; + + invoke('update_game_directory', { path: gameDir }).catch(err => + console.error('Failed to push game directory to backend:', err), + ); }; void sync(); - invoke('update_game_directory', { path: gameDir }).catch(err => - console.error('Failed to push game directory to backend:', err), - ); + return () => { + cancelled = true; + }; }, [gameDir]); const rescan = useCallback(() => { - if (!gameDir) return; - invoke('update_game_directory', { path: gameDir }).catch(err => - console.error('Failed to rescan game directory:', err), - ); + if (!gameDir.trim()) { + setGameDirExists(false); + return; + } + const sync = async () => { + let exists = false; + try { + exists = await invoke('game_directory_exists', { path: gameDir }); + } catch (err) { + console.error('Failed to validate game directory:', err); + } + setGameDirExists(exists); + if (!exists) return; + + invoke('update_game_directory', { path: gameDir }).catch(err => + console.error('Failed to rescan game directory:', err), + ); + }; + void sync(); }, [gameDir]); - return { gameDir, setGameDir, rescan }; + return { gameDir, gameDirExists, setGameDir, rescan }; }; diff --git a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css index 242c9ea..7a29a19 100644 --- a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css +++ b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css @@ -1718,6 +1718,9 @@ margin: 0 0 20px; max-width: 44ch; } +.empty-state-title + .ghost-btn { + margin-top: 14px; +} .empty-state .ghost-btn { background: var(--accent); color: white; diff --git a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx index 837705c..947a84a 100644 --- a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx +++ b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx @@ -45,7 +45,7 @@ const openLogsWindow = async () => { export const MainWindow = () => { const { settings, set: setSetting } = useSettings(); - const { gameDir, setGameDir, rescan } = useGameDirectory(); + const { gameDir, gameDirExists, setGameDir, rescan } = useGameDirectory(); const games = useGames(rescan); const actions = useGameActions(games, settings); const thumbnails = useThumbnails(); @@ -54,13 +54,18 @@ export const MainWindow = () => { const [removeGameId, setRemoveGameId] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); - const counts = useMemo(() => countByFilter(games.games), [games.games]); + const hasGameDirectory = !!gameDir.trim() && gameDirExists; + const visibleGames = useMemo( + () => hasGameDirectory ? games.games : [], + [games.games, hasGameDirectory], + ); + const counts = useMemo(() => countByFilter(visibleGames), [visibleGames]); // Query is local UI state (no need to persist). const [query, setQuery] = useState(''); const filteredGames = useMemo( - () => applyFilterAndSort(games.games, settings.filter, settings.sort, query), - [games.games, settings.filter, settings.sort, query], + () => applyFilterAndSort(visibleGames, settings.filter, settings.sort, query), + [visibleGames, settings.filter, settings.sort, query], ); const openGame = useMemo( @@ -116,25 +121,26 @@ export const MainWindow = () => { return (
- {gameDir ? ( - <> - setSetting('filter', v)} - counts={counts} - query={query} - setQuery={setQuery} - sort={settings.sort} - setSort={(v) => setSetting('sort', v)} - gameDir={gameDir} - onPickDirectory={() => void pickDirectory()} - kebabItems={kebabItems} - /> -
+ setSetting('filter', v)} + counts={counts} + query={query} + setQuery={setQuery} + sort={settings.sort} + setSort={(v) => setSetting('sort', v)} + gameDir={gameDir} + gameDirExists={gameDirExists} + onPickDirectory={() => void pickDirectory()} + kebabItems={kebabItems} + /> +
+ {hasGameDirectory ? ( + <> {filteredGames.length === 0 ? ( - games.games.length === 0 ? ( + visibleGames.length === 0 ? ( { onCancelDownload={(g) => actions.cancelDownload(g.id)} /> )} -
- - ) : ( -
+ + ) : ( void pickDirectory()} /> -
- )} + )} +
{openGame && (