2e7a0cff2f
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
93 lines
3.0 KiB
TypeScript
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 };
|
|
};
|