|
|
@@ -143,6 +143,30 @@ fn navigate_webview(
|
|
|
webview.navigate(parsed).map_err(|e| e.to_string())
|
|
|
}
|
|
|
|
|
|
+/// 在子 webview 上执行历史导航 / 刷新动作。
|
|
|
+///
|
|
|
+/// 通过 webview.eval 跑很短的一行 JS,避开各平台 API 差异:
|
|
|
+/// - "back" → history.back()
|
|
|
+/// - "forward" → history.forward()
|
|
|
+/// - "reload" → location.reload()
|
|
|
+///
|
|
|
+/// 不主动重置 page_stage:用户点 Back 回到中间页 / 点 Refresh 重载最终页时,
|
|
|
+/// 状态机已经是 Final/Ready,handle_page_started/loaded 会按当前 stage 静默处理
|
|
|
+/// (即不会重新启动 landing 轮询,也不会重复触发自动截图)。如果用户想完整重跑
|
|
|
+/// landing 流程,应通过左侧任务列表再点一下任务(走 navigate_webview,那个命令
|
|
|
+/// 会把 stage 重置为 Initial)。
|
|
|
+#[tauri::command]
|
|
|
+fn webview_history_action(app: tauri::AppHandle, action: String) -> Result<(), String> {
|
|
|
+ let script = match action.as_str() {
|
|
|
+ "back" => "history.back()",
|
|
|
+ "forward" => "history.forward()",
|
|
|
+ "reload" => "location.reload()",
|
|
|
+ other => return Err(format!("未知的 webview 历史动作: {other}")),
|
|
|
+ };
|
|
|
+ let webview = find_content_webview(&app)?;
|
|
|
+ webview.eval(script).map_err(|e| e.to_string())
|
|
|
+}
|
|
|
+
|
|
|
/// 修改主窗口的「客户区」尺寸(逻辑像素,不含系统 titlebar / 边框)。
|
|
|
///
|
|
|
/// 前端传进来的 (width, height) 含义是「左栏 + 工作区宽」「工具栏高 + 工作区高」,
|
|
|
@@ -179,56 +203,93 @@ fn set_window_size(
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
-/// 子 webview 任意一次「页面加载完成」时的总入口(由 on_page_load 异步派发)。
|
|
|
+/// PageLoadEvent::Started 处理:尽早启动 landing 轮询。
|
|
|
+///
|
|
|
+/// 之所以不等到 Finished(= window.onload)才启动,是因为 onload 必须等所有图片 /
|
|
|
+/// 广告 / 第三方脚本加载完成,而 `div.c-tags`、`.menu-float__content` 这些节点在
|
|
|
+/// HTML parse 阶段就已经进 DOM 了。Started 触发时新文档刚提交,立即跑 CDP 轮询
|
|
|
+/// 就能在几百毫秒内捕获到目标节点,无需等几秒的尾资源。
|
|
|
///
|
|
|
-/// 根据 page_stage 分流:
|
|
|
-/// - Initial:当前是中间页 → spawn landing 轮询;
|
|
|
-/// - Final:当前是 Visit Site 最终页 → emit task-page-ready,1.5s 后自动截图;
|
|
|
-/// - Ready:已就绪还在加载(重新刷新等),不做额外动作。
|
|
|
+/// 平台说明:landing 轮询仅 Windows 实现(CDP 路径)。macOS 这里 no-op,靠
|
|
|
+/// handle_page_loaded 的 Finished 分支把 Initial 当 Final 处理。
|
|
|
+async fn handle_page_started(app: AppHandle, loaded_url: String) {
|
|
|
+ let (task_id, stage) = match read_task_and_stage(&app) {
|
|
|
+ Some(v) => v,
|
|
|
+ None => return,
|
|
|
+ };
|
|
|
+
|
|
|
+ if stage != PageStage::Initial {
|
|
|
+ // Final / Ready 阶段下的 Started(比如 final URL 已开始重定向),交给 Finished 处理
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(target_os = "windows")]
|
|
|
+ {
|
|
|
+ landing::spawn_polling(app, task_id, loaded_url);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(not(target_os = "windows"))]
|
|
|
+ {
|
|
|
+ // macOS:什么都不做,等 Finished 一起处理
|
|
|
+ let _ = (app, task_id, loaded_url);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// PageLoadEvent::Finished 处理:把 stage 推进到 Ready + 触发自动截图。
|
|
|
///
|
|
|
-/// about:blank / 无任务上下文(current_task_id=None)的加载直接忽略。
|
|
|
+/// 分流:
|
|
|
+/// - Initial:Windows 下不动(轮询负责);macOS 直接 promote_to_ready(无 CDP fallback)。
|
|
|
+/// - Final :跨平台都 promote_to_ready。
|
|
|
+/// - Ready :忽略(用户刷新等同 URL 重新加载)。
|
|
|
async fn handle_page_loaded(app: AppHandle, loaded_url: String) {
|
|
|
- use tauri::Emitter;
|
|
|
+ let (task_id, stage) = match read_task_and_stage(&app) {
|
|
|
+ Some(v) => v,
|
|
|
+ None => return,
|
|
|
+ };
|
|
|
|
|
|
- let (task_id, stage) = {
|
|
|
- let Some(state) = app.try_state::<AppState>() else {
|
|
|
- return;
|
|
|
- };
|
|
|
- let Some(id) = state.current_task() else {
|
|
|
- return; // about:blank / 还没选任务
|
|
|
- };
|
|
|
- let stage = state
|
|
|
- .page_stage
|
|
|
- .lock()
|
|
|
- .ok()
|
|
|
- .map(|g| *g)
|
|
|
- .unwrap_or(PageStage::Initial);
|
|
|
- (id, stage)
|
|
|
+ let should_promote = match stage {
|
|
|
+ PageStage::Final => true,
|
|
|
+ // 非 Windows 没有 CDP,Initial 阶段也没有 Visit Site 自动跳转,
|
|
|
+ // 把 Finished 当作就绪信号;window.onload 已触发,页面已可见,
|
|
|
+ // 不会出现「按钮就绪但页面白屏」的尴尬。
|
|
|
+ #[cfg(not(target_os = "windows"))]
|
|
|
+ PageStage::Initial => true,
|
|
|
+ _ => false,
|
|
|
};
|
|
|
|
|
|
- match stage {
|
|
|
- PageStage::Initial => {
|
|
|
- // landing 模块自己负责 emit tags / 触发 Visit Site 跳转 / 超时
|
|
|
- landing::spawn_polling(app, task_id, loaded_url);
|
|
|
- }
|
|
|
- PageStage::Final => {
|
|
|
- // 已跳到最终页:推进到 Ready + 通知前端解锁按钮 + 等 1.5s 后自动截图
|
|
|
- if let Some(state) = app.try_state::<AppState>() {
|
|
|
- if let Ok(mut g) = state.page_stage.lock() {
|
|
|
- *g = PageStage::Ready;
|
|
|
- }
|
|
|
- }
|
|
|
- let _ = app.emit(
|
|
|
- "task-page-ready",
|
|
|
- serde_json::json!({ "taskId": task_id, "url": loaded_url }),
|
|
|
- );
|
|
|
- tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
|
|
- capture::trigger_auto_capture(app).await;
|
|
|
- }
|
|
|
- PageStage::Ready => {
|
|
|
- // 已就绪状态下二次加载(用户刷新页面等)—— 不做额外动作
|
|
|
+ if should_promote {
|
|
|
+ promote_to_ready(&app, &task_id, &loaded_url).await;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// 从 AppState 读 (task_id, stage) 的小帮手;无任务上下文(about:blank 等)返回 None。
|
|
|
+fn read_task_and_stage(app: &AppHandle) -> Option<(String, PageStage)> {
|
|
|
+ let state = app.try_state::<AppState>()?;
|
|
|
+ let task_id = state.current_task()?;
|
|
|
+ let stage = state
|
|
|
+ .page_stage
|
|
|
+ .lock()
|
|
|
+ .ok()
|
|
|
+ .map(|g| *g)
|
|
|
+ .unwrap_or(PageStage::Initial);
|
|
|
+ Some((task_id, stage))
|
|
|
+}
|
|
|
+
|
|
|
+/// 把 stage 切到 Ready + emit `task-page-ready` + 1.5s 后触发自动截图。
|
|
|
+async fn promote_to_ready(app: &AppHandle, task_id: &str, loaded_url: &str) {
|
|
|
+ use tauri::Emitter;
|
|
|
+
|
|
|
+ if let Some(state) = app.try_state::<AppState>() {
|
|
|
+ if let Ok(mut g) = state.page_stage.lock() {
|
|
|
+ *g = PageStage::Ready;
|
|
|
}
|
|
|
}
|
|
|
+ let _ = app.emit(
|
|
|
+ "task-page-ready",
|
|
|
+ serde_json::json!({ "taskId": task_id, "url": loaded_url }),
|
|
|
+ );
|
|
|
+ tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
|
|
+ capture::trigger_auto_capture(app.clone()).await;
|
|
|
}
|
|
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
|
@@ -253,6 +314,35 @@ pub fn run() {
|
|
|
sql: "ALTER TABLE tasks ADD COLUMN tags TEXT NOT NULL DEFAULT '';",
|
|
|
kind: tauri_plugin_sql::MigrationKind::Up,
|
|
|
},
|
|
|
+ // v3~v6:补三个产物字段 + status 普通索引。拆成单语句条目避免不同 sqlx 版本
|
|
|
+ // 对多语句 migration 的处理差异(id 已是 PRIMARY KEY,自带唯一索引不重复建)。
|
|
|
+ // - site_url:landing 跳转后落地的最终 URL
|
|
|
+ // - pic / video:最近一次截图 / 录制 mp4 的文件名(仅 basename)
|
|
|
+ // - idx_tasks_status:主列表 WHERE status=0 谓词的非唯一索引
|
|
|
+ tauri_plugin_sql::Migration {
|
|
|
+ version: 3,
|
|
|
+ description: "add_tasks_site_url",
|
|
|
+ sql: "ALTER TABLE tasks ADD COLUMN site_url TEXT NOT NULL DEFAULT '';",
|
|
|
+ kind: tauri_plugin_sql::MigrationKind::Up,
|
|
|
+ },
|
|
|
+ tauri_plugin_sql::Migration {
|
|
|
+ version: 4,
|
|
|
+ description: "add_tasks_pic",
|
|
|
+ sql: "ALTER TABLE tasks ADD COLUMN pic TEXT NOT NULL DEFAULT '';",
|
|
|
+ kind: tauri_plugin_sql::MigrationKind::Up,
|
|
|
+ },
|
|
|
+ tauri_plugin_sql::Migration {
|
|
|
+ version: 5,
|
|
|
+ description: "add_tasks_video",
|
|
|
+ sql: "ALTER TABLE tasks ADD COLUMN video TEXT NOT NULL DEFAULT '';",
|
|
|
+ kind: tauri_plugin_sql::MigrationKind::Up,
|
|
|
+ },
|
|
|
+ tauri_plugin_sql::Migration {
|
|
|
+ version: 6,
|
|
|
+ description: "create_idx_tasks_status",
|
|
|
+ sql: "CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);",
|
|
|
+ kind: tauri_plugin_sql::MigrationKind::Up,
|
|
|
+ },
|
|
|
];
|
|
|
|
|
|
tauri::Builder::default()
|
|
|
@@ -281,10 +371,13 @@ pub fn run() {
|
|
|
|
|
|
// 2) 在已经放大的窗口上挂 child webview,定位到工具栏下方
|
|
|
//
|
|
|
- // on_page_load 钩子分流到 [`handle_page_loaded`]:
|
|
|
- // - Initial 阶段:spawn landing 轮询(找 tags + Visit Site 链接)
|
|
|
- // - Final 阶段:标记 Ready + 1.5s 后自动截图
|
|
|
- // - Ready 阶段:忽略(用户手动刷新 / 同 URL 重新加载)
|
|
|
+ // on_page_load 同时处理 Started / Finished 两个事件:
|
|
|
+ // - Started :导航刚开始,新文档刚提交(DOM 开始 parse)。Initial 阶段
|
|
|
+ // 立即 spawn landing 轮询——比等到 Finished (window.onload)
|
|
|
+ // 能早好几秒,因为 c-tags / Visit Site 链接 parse 阶段就在 DOM 里。
|
|
|
+ // - Finished:window.onload,所有资源到位。这里只用来分流 Final → Ready
|
|
|
+ // + 触发自动截图。
|
|
|
+ // macOS 没 CDP,把 Initial 也当作 Final 处理,直接 Ready。
|
|
|
let initial_url: url::Url = INITIAL_URL.parse()?;
|
|
|
let app_handle_for_load = app.handle().clone();
|
|
|
let window = app.get_window("main").ok_or("主窗口未找到")?;
|
|
|
@@ -293,14 +386,21 @@ pub fn run() {
|
|
|
WebviewBuilder::new(CONTENT_WEBVIEW_LABEL, WebviewUrl::External(initial_url))
|
|
|
.on_page_load(move |_webview, payload| {
|
|
|
use tauri::webview::PageLoadEvent;
|
|
|
- if !matches!(payload.event(), PageLoadEvent::Finished) {
|
|
|
- return;
|
|
|
- }
|
|
|
let app = app_handle_for_load.clone();
|
|
|
let loaded_url = payload.url().to_string();
|
|
|
- tauri::async_runtime::spawn(async move {
|
|
|
- handle_page_loaded(app, loaded_url).await;
|
|
|
- });
|
|
|
+ match payload.event() {
|
|
|
+ PageLoadEvent::Started => {
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ handle_page_started(app, loaded_url).await;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ PageLoadEvent::Finished => {
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ handle_page_loaded(app, loaded_url).await;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ _ => {}
|
|
|
+ }
|
|
|
}),
|
|
|
LogicalPosition::new(0, 0),
|
|
|
LogicalSize::new(
|
|
|
@@ -316,6 +416,7 @@ pub fn run() {
|
|
|
})
|
|
|
.invoke_handler(tauri::generate_handler![
|
|
|
navigate_webview,
|
|
|
+ webview_history_action,
|
|
|
set_window_size,
|
|
|
query_task_assets,
|
|
|
capture::capture_page,
|