|
|
@@ -1,15 +1,23 @@
|
|
|
-import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
|
+import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
|
+import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
|
import { openUrl, revealItemInDir } from "@tauri-apps/plugin-opener";
|
|
|
import { Button, ConfigProvider, Divider, InputNumber, Layout, Menu, notification, Space, theme, Tooltip } from "antd";
|
|
|
-import { mockTasks } from "./mocks/tasks";
|
|
|
+import {
|
|
|
+ countPendingTasks,
|
|
|
+ listPendingTasks,
|
|
|
+ markTaskDone,
|
|
|
+ type Task,
|
|
|
+} from "./lib/tasks";
|
|
|
import {
|
|
|
EVT_RECORDING_FAILED,
|
|
|
EVT_RECORDING_FINISHED,
|
|
|
EVT_RECORD_SHORTCUT,
|
|
|
EVT_SCREENSHOT_FAILED,
|
|
|
EVT_SCREENSHOT_FINISHED,
|
|
|
+ EVT_TASKS_IMPORTED,
|
|
|
type RecordingFailedPayload,
|
|
|
type RecordingFinishedPayload,
|
|
|
type RecordShortcutPayload,
|
|
|
@@ -29,6 +37,7 @@ import {
|
|
|
import "./App.css";
|
|
|
import {
|
|
|
BankOutlined,
|
|
|
+ CheckCircleOutlined,
|
|
|
CloseOutlined,
|
|
|
FileImageOutlined,
|
|
|
FolderOpenOutlined,
|
|
|
@@ -81,6 +90,9 @@ function App() {
|
|
|
// 当前激活的任务 id(仅用于左栏视觉高亮)
|
|
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
|
|
|
|
+ // 待处理任务(status=0)的真实列表,来自 sqlite
|
|
|
+ const [tasks, setTasks] = useState<Task[]>([]);
|
|
|
+
|
|
|
const [contentSize, setContentSize] = useState<{ w: number; h: number }>({ w: 1280, h: 720 });
|
|
|
const contentSizeRef = useRef(contentSize);
|
|
|
const [customMode, setCustomMode] = useState(false);
|
|
|
@@ -103,8 +115,62 @@ function App() {
|
|
|
// antd 6 notification 必须用 hook + contextHolder 才能正确取到主题
|
|
|
const [notifyApi, notifyContext] = notification.useNotification();
|
|
|
|
|
|
+ /**
|
|
|
+ * 重新拉取 sqlite 中 status=0 的任务列表。
|
|
|
+ * 调用场景:
|
|
|
+ * - 首次挂载
|
|
|
+ * - 监听到 EVT_TASKS_IMPORTED(导入窗口写入完)
|
|
|
+ * - "完成" 按钮把当前任务标记为已完成后
|
|
|
+ */
|
|
|
+ const reloadTasks = useCallback(async (): Promise<Task[]> => {
|
|
|
+ try {
|
|
|
+ const list = await listPendingTasks();
|
|
|
+ setTasks(list);
|
|
|
+ return list;
|
|
|
+ } catch (e) {
|
|
|
+ console.error("加载任务列表失败:", e);
|
|
|
+ notifyApi.error({
|
|
|
+ message: "加载任务失败",
|
|
|
+ description: String(e),
|
|
|
+ duration: 6,
|
|
|
+ });
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }, [notifyApi]);
|
|
|
+
|
|
|
+ /** 启动「批量导入 URL」窗口(独立 WebviewWindow,label = "import") */
|
|
|
+ const openImportWindow = useCallback(async () => {
|
|
|
+ try {
|
|
|
+ // 若已存在直接 setFocus,避免重复创建
|
|
|
+ const existing = await WebviewWindow.getByLabel("import");
|
|
|
+ if (existing) {
|
|
|
+ await existing.setFocus();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const w = new WebviewWindow("import", {
|
|
|
+ url: "import.html",
|
|
|
+ title: "导入 URL",
|
|
|
+ width: 560,
|
|
|
+ height: 480,
|
|
|
+ resizable: true,
|
|
|
+ // 主窗口是 decorations: false,这里给原生标题栏方便用户拖动/关闭
|
|
|
+ decorations: true,
|
|
|
+ });
|
|
|
+ w.once("tauri://error", (e) => {
|
|
|
+ console.error("导入窗口创建失败:", e);
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ console.error("openImportWindow 失败:", e);
|
|
|
+ notifyApi.error({
|
|
|
+ message: "无法打开导入窗口",
|
|
|
+ description: String(e),
|
|
|
+ duration: 6,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, [notifyApi]);
|
|
|
+
|
|
|
function handleSelectTask({ key }: { key: string }) {
|
|
|
- const t = mockTasks.find((v) => v.id == key);
|
|
|
+ const t = tasks.find((v) => v.id == key);
|
|
|
if (!t) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -132,6 +198,67 @@ function App() {
|
|
|
void refreshAssets(activeId);
|
|
|
}, [activeId, refreshAssets]);
|
|
|
|
|
|
+ // 首次挂载:从 sqlite 加载待处理任务;若为空则拉起「批量导入 URL」窗口。
|
|
|
+ // 依赖项故意只放 reloadTasks / openImportWindow(都用 useCallback 稳定引用),
|
|
|
+ // 避免 tasks 变化导致再次拉起导入窗口。
|
|
|
+ useEffect(() => {
|
|
|
+ (async () => {
|
|
|
+ const pending = await countPendingTasks().catch(() => 0);
|
|
|
+ const list = await reloadTasks();
|
|
|
+ if (pending === 0 && list.length === 0) {
|
|
|
+ await openImportWindow();
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 监听导入窗口发出的 tasks-imported 事件,刷新主列表
|
|
|
+ useEffect(() => {
|
|
|
+ let unlisten: UnlistenFn | null = null;
|
|
|
+ let disposed = false;
|
|
|
+ (async () => {
|
|
|
+ const u = await listen<{ count: number }>(EVT_TASKS_IMPORTED, (e) => {
|
|
|
+ notifyApi.success({
|
|
|
+ message: "导入完成",
|
|
|
+ description: `已写入 ${e.payload.count} 条任务`,
|
|
|
+ duration: 3,
|
|
|
+ });
|
|
|
+ void reloadTasks();
|
|
|
+ });
|
|
|
+ if (disposed) u();
|
|
|
+ else unlisten = u;
|
|
|
+ })();
|
|
|
+ return () => {
|
|
|
+ disposed = true;
|
|
|
+ unlisten?.();
|
|
|
+ };
|
|
|
+ }, [notifyApi, reloadTasks]);
|
|
|
+
|
|
|
+ /** 工具栏 "完成" 按钮:把当前激活任务标记为已完成(status=1)→ 刷新 → 清空选中 */
|
|
|
+ const handleCompleteTask = useCallback(async () => {
|
|
|
+ if (!activeId) return;
|
|
|
+ try {
|
|
|
+ await markTaskDone(activeId);
|
|
|
+ notifyApi.success({
|
|
|
+ message: "任务已完成",
|
|
|
+ description: `任务 ${activeId} 已从列表移除`,
|
|
|
+ duration: 3,
|
|
|
+ });
|
|
|
+ setActiveId(null);
|
|
|
+ const list = await reloadTasks();
|
|
|
+ // 若已无任务,自动再拉起一次导入窗口(用户可继续粘贴)
|
|
|
+ if (list.length === 0) {
|
|
|
+ await openImportWindow();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ notifyApi.error({
|
|
|
+ message: "标记完成失败",
|
|
|
+ description: String(e),
|
|
|
+ duration: 6,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, [activeId, reloadTasks, openImportWindow, notifyApi]);
|
|
|
+
|
|
|
/** 手动截图:交给 Rust 命令,结果通过事件回推(统一与自动截图的提示路径) */
|
|
|
const handleManualCapture = useCallback(async () => {
|
|
|
if (!activeId) return;
|
|
|
@@ -444,14 +571,54 @@ function App() {
|
|
|
|
|
|
const [statusColor, setStatusColor] = useState("#888");
|
|
|
const [statusText, setStatusText] = useState("请选择任务");
|
|
|
+
|
|
|
+ // 当前窗口句柄,用于自定义标题栏的最小化 / 关闭
|
|
|
+ const appWindow = useMemo(() => getCurrentWindow(), []);
|
|
|
+
|
|
|
+ const handleAppClose = useCallback(async () => {
|
|
|
+ try {
|
|
|
+ await appWindow.close();
|
|
|
+ } catch (e) {
|
|
|
+ console.error("appWindow.close 失败:", e);
|
|
|
+ }
|
|
|
+ }, [appWindow]);
|
|
|
+
|
|
|
+ const handleAppMinimize = useCallback(async () => {
|
|
|
+ try {
|
|
|
+ await appWindow.minimize();
|
|
|
+ } catch (e) {
|
|
|
+ console.error("appWindow.minimize 失败:", e);
|
|
|
+ }
|
|
|
+ }, [appWindow]);
|
|
|
+
|
|
|
return (
|
|
|
<Layout className="m-0 p-0 h-full overflow-hidden">
|
|
|
- <Header className="app-title h-8 select-none" style={{ paddingLeft: 8 }} >
|
|
|
- <div className="flex h-full flex-row items-center">
|
|
|
+ {/* data-tauri-drag-region:Tauri 2 原生支持,webview 层会自动 hook mousedown
|
|
|
+ * 触发系统级拖窗,比手写 startDragging 更稳定(Windows 尤其如此)。 */}
|
|
|
+ <Header
|
|
|
+ className="app-title h-8 select-none"
|
|
|
+ style={{ paddingLeft: 8 }}
|
|
|
+ data-tauri-drag-region
|
|
|
+ >
|
|
|
+ <div className="flex h-full flex-row items-center" data-tauri-drag-region>
|
|
|
<Space size={4}>
|
|
|
- <Button className="app-close" icon={<CloseOutlined />} size="large" type="text" />
|
|
|
+ <Button
|
|
|
+ className="app-close"
|
|
|
+ icon={<CloseOutlined />}
|
|
|
+ size="large"
|
|
|
+ type="text"
|
|
|
+ data-tauri-drag-region="no-drag"
|
|
|
+ onClick={handleAppClose}
|
|
|
+ />
|
|
|
<Button icon={<BankOutlined />} disabled size="large" type="text" />
|
|
|
- <Button className="app-minimize" icon={<MinusOutlined />} size="large" type="text" />
|
|
|
+ <Button
|
|
|
+ className="app-minimize"
|
|
|
+ icon={<MinusOutlined />}
|
|
|
+ size="large"
|
|
|
+ type="text"
|
|
|
+ data-tauri-drag-region="no-drag"
|
|
|
+ onClick={handleAppMinimize}
|
|
|
+ />
|
|
|
</Space>
|
|
|
<div className="w-32" />
|
|
|
<Tooltip title={activeId ? "截图整页(覆盖之前的自动截图)" : "请先选择任务"} placement="right">
|
|
|
@@ -491,6 +658,18 @@ function App() {
|
|
|
onClick={() => void handleStopRecord()}
|
|
|
/>
|
|
|
</Tooltip>
|
|
|
+ {/* 完成按钮:把当前任务在 sqlite 标记为 status=1,从主列表移除 */}
|
|
|
+ <Tooltip
|
|
|
+ title={activeId ? "标记当前任务为已完成" : "请先选择任务"}
|
|
|
+ placement="right"
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ shape="circle"
|
|
|
+ icon={<CheckCircleOutlined />}
|
|
|
+ disabled={!activeId}
|
|
|
+ onClick={() => void handleCompleteTask()}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
<Divider orientation="vertical" />
|
|
|
{/* 打开文件夹:reveal mp4 → png → 兜底 screenshots 目录 */}
|
|
|
<Tooltip
|
|
|
@@ -618,7 +797,7 @@ function App() {
|
|
|
onClick={handleSelectTask}
|
|
|
|
|
|
>
|
|
|
- {mockTasks.map((t) => <Menu.Item icon={<LinkOutlined />} key={t.id}>
|
|
|
+ {tasks.map((t) => <Menu.Item icon={<LinkOutlined />} key={t.id}>
|
|
|
{t.id}
|
|
|
</Menu.Item>
|
|
|
)}
|