Files
lanspread/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts
T
ddidderr 2e7a0cff2f feat(ui): move game folder picker into settings
The design update moved game-folder configuration out of launcher chrome and
into Settings > Library. Follow that contract in the runtime UI without
changing the existing storage or Tauri directory commands.

The top bar now leaves its right edge for the kebab menu. Settings owns a new
Game folder row that shows a valid selected path with a neutral Change button,
or the red Not set state with a stronger Choose button when no accessible
directory is configured. Both the empty-library state and the Settings row
still use the existing native directory picker, so existing saved paths and
rescans keep their current behavior.

Keep useGameDirectory as the directory-state owner and expose the shared
hasGameDirectory boolean from that hook so the grid and Settings field agree on
what counts as configured.

Test Plan:
- git diff --cached --check
- just frontend-test
- just build

Refs: 62b409f4bfc4995c25461776107d28f52b24f30e
2026-05-21 21:32:29 +02:00

93 lines
3.0 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { load } from '@tauri-apps/plugin-store';
import { GAME_DIR_KEY, SETTINGS_FILE, SETTINGS_FILE_OPTIONS } from '../lib/store';
/**
* Owns the user's selected game directory. Hydrates from the persistent store
* on mount, writes back on every change, and pushes the value to the Tauri
* backend so it can scan/rescan.
*/
export const useGameDirectory = () => {
const [gameDir, setGameDir] = useState('');
const [gameDirExists, setGameDirExists] = useState(false);
useEffect(() => {
let cancelled = false;
const hydrate = async () => {
try {
const store = await load(SETTINGS_FILE, SETTINGS_FILE_OPTIONS);
const saved = await store.get<string>(GAME_DIR_KEY);
if (saved && !cancelled) setGameDir(saved);
} catch (err) {
console.error('Failed to load game directory:', err);
}
};
void hydrate();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!gameDir.trim()) {
setGameDirExists(false);
return;
}
let cancelled = false;
const sync = async () => {
try {
const store = await load(SETTINGS_FILE, SETTINGS_FILE_OPTIONS);
await store.set(GAME_DIR_KEY, gameDir);
} catch (err) {
console.error('Failed to persist game directory:', err);
}
let exists = false;
try {
exists = await invoke<boolean>('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();
return () => {
cancelled = true;
};
}, [gameDir]);
const hasGameDirectory = gameDir.trim() !== '' && gameDirExists;
const rescan = useCallback(() => {
if (!gameDir.trim()) {
setGameDirExists(false);
return;
}
const sync = async () => {
let exists = false;
try {
exists = await invoke<boolean>('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, gameDirExists, hasGameDirectory, setGameDir, rescan };
};