9f5c84ee71
参考成熟桌面项目管理器重构紧凑项目表、搜索和行尾菜单 支持Windows路径及Godot根目录和一层子目录工程识别与导入 修复1280×720页面溢出、列表滚动和末行菜单裁切 补充项目页回归测试、PRD、技术方案和项目记忆
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
import {
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { resolveTauriInvoke } from '../../app/tauri';
|
|
import type { LocalProjectDirectoryStatus } from '../../app/types';
|
|
import {
|
|
isAbsoluteProjectPath,
|
|
projectPathHasControlCharacter,
|
|
} from '../project-summary/projectSummary';
|
|
import {
|
|
buildRecentProjectRows,
|
|
readRecentWorkspaces,
|
|
removeRecentWorkspace,
|
|
writeRecentWorkspace,
|
|
} from './model';
|
|
|
|
export function useRecentProjects(setStatus: Dispatch<SetStateAction<string>>) {
|
|
const [recentWorkspaces, setRecentWorkspaces] =
|
|
useState<string[]>(readRecentWorkspaces);
|
|
const [recentWorkspaceStatuses, setRecentWorkspaceStatuses] = useState<
|
|
Record<string, LocalProjectDirectoryStatus | null>
|
|
>({});
|
|
const [recentWorkspaceRefreshKey, setRecentWorkspaceRefreshKey] = useState(0);
|
|
const [recentWorkspaceRefreshing, setRecentWorkspaceRefreshing] =
|
|
useState(false);
|
|
const [projectSearchQuery, setProjectSearchQuery] = useState('');
|
|
|
|
useEffect(() => {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke || recentWorkspaces.length === 0) {
|
|
setRecentWorkspaceStatuses({});
|
|
setRecentWorkspaceRefreshing(false);
|
|
return;
|
|
}
|
|
let disposed = false;
|
|
setRecentWorkspaceRefreshing(true);
|
|
void Promise.all(
|
|
recentWorkspaces.map(async (workspace) => {
|
|
try {
|
|
const result = await invoke<LocalProjectDirectoryStatus>(
|
|
'inspect_local_project_directory',
|
|
{ projectPath: workspace },
|
|
);
|
|
return [workspace, result] as const;
|
|
} catch {
|
|
return [workspace, null] as const;
|
|
}
|
|
}),
|
|
).then((entries) => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
setRecentWorkspaceStatuses(Object.fromEntries(entries));
|
|
setRecentWorkspaceRefreshing(false);
|
|
});
|
|
return () => {
|
|
disposed = true;
|
|
};
|
|
}, [recentWorkspaces, recentWorkspaceRefreshKey]);
|
|
|
|
function rememberRecentWorkspace(projectPath: string) {
|
|
setRecentWorkspaces(writeRecentWorkspace(projectPath));
|
|
setRecentWorkspaceRefreshKey((current) => current + 1);
|
|
}
|
|
|
|
function handleRecentWorkspaceRemove(projectPath: string) {
|
|
setRecentWorkspaces(removeRecentWorkspace(projectPath));
|
|
setRecentWorkspaceStatuses((current) => {
|
|
const { [projectPath]: _removed, ...rest } = current;
|
|
return rest;
|
|
});
|
|
}
|
|
|
|
async function handleRevealProjectDirectory(projectPath: string) {
|
|
const trimmedProjectPath = projectPath.trim();
|
|
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
|
setStatus('请提供项目绝对路径');
|
|
return;
|
|
}
|
|
if (projectPathHasControlCharacter(trimmedProjectPath)) {
|
|
setStatus('项目目录不能包含控制字符');
|
|
return;
|
|
}
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
setStatus('需要在 Tauri App 内运行');
|
|
return;
|
|
}
|
|
try {
|
|
await invoke('open_local_project_directory', {
|
|
projectPath: trimmedProjectPath,
|
|
});
|
|
setStatus('已打开项目目录');
|
|
} catch (error) {
|
|
setStatus(error instanceof Error ? error.message : String(error));
|
|
}
|
|
}
|
|
|
|
const projectRows = buildRecentProjectRows(
|
|
recentWorkspaces,
|
|
recentWorkspaceStatuses,
|
|
recentWorkspaceRefreshing,
|
|
);
|
|
const filteredProjectRows = useMemo(() => {
|
|
const normalizedQuery = projectSearchQuery.trim().toLocaleLowerCase();
|
|
if (!normalizedQuery) {
|
|
return projectRows;
|
|
}
|
|
return projectRows.filter((project) =>
|
|
[
|
|
project.name,
|
|
project.path,
|
|
project.status,
|
|
project.godotProjectRoot ?? '',
|
|
project.projectKind === 'godot'
|
|
? 'godot'
|
|
: project.projectKind === 'web'
|
|
? 'gameagent web'
|
|
: 'unknown',
|
|
].some((value) => value.toLocaleLowerCase().includes(normalizedQuery)),
|
|
);
|
|
}, [projectRows, projectSearchQuery]);
|
|
|
|
return {
|
|
recentWorkspaces,
|
|
recentWorkspaceRefreshing,
|
|
projectRows,
|
|
filteredProjectRows,
|
|
projectSearchQuery,
|
|
setProjectSearchQuery,
|
|
recentProjectRows: projectRows
|
|
.filter((project) => project.canOpen)
|
|
.slice(0, 3),
|
|
rememberRecentWorkspace,
|
|
handleRecentWorkspaceRemove,
|
|
handleRevealProjectDirectory,
|
|
};
|
|
}
|
|
|
|
export type RecentProjectsController = ReturnType<typeof useRecentProjects>;
|