feat(油猴): 行内推送按钮 + 已推送画廊标灰

需求两条:
  1) 列表页的复选框改成按钮,点一下直接推送到工具
  2) 推送过但没点进去的画廊,标题也要变灰(像浏览过的),
     这样刷新后还认得出哪些推过

一、行内推送按钮

复选框 + 底部「批量推送」两步走太啰嗦,列表页每行改成「📤 推送」
胶囊按钮,点一下直接调 /add。按钮三态:
  📤 推送  ->  ⏳ 推送中(禁用)  ->  ✓ 已推送(变绿)
底部悬浮条保留,改为「全部推送」+「重置状态」+ 本页计数。

二、标灰::visited 伪造不了,只能自己记

E-Hentai 的灰色来自自带样式
  a:visited .glink, a:active .glink { color:#8F6063 }
但 :visited 是浏览器**受保护**伪类:脚本查 matches(':visited')
永远返回 false,加 class 也触发不了站点的原生规则。

所以改成自己记一份「推送过」的清单,再照抄站点的色值手动染色,
观感与原生 :visited 一致(hover 恢复站点的 #8F4701)。

三、清单存哪:工具端为主,浏览器端为辅

新增 internal/core/pushed.go —— PushedStore 独立于 Store,
单独存 %APPDATA%\EH Downloader\pushed.json。

为什么单独一个文件:state.json 里的 tasks 是「当前要下载的东西」,
用户点「清除已完成」就删掉了;但「这个画廊我推送过」是历史事实,
删了任务不该忘掉。所以推送历史与任务列表解耦。

新增桥口 GET /pushed 返回推送历史(与 GET /tasks 语义不同:
后者是当前任务列表,会被清除)。

油猴脚本侧:
  - localStorage(ehdl_pushed_gids)先渲染,刷新后立刻可见,
    工具不在线也能用
  - 工具在线时用 /pushed 校正一次(localStorage 可能被清,
    工具才是权威),以工具为准整体覆盖
  - GET /tasks 保留给需要看当前任务列表的场合

四、附带:state.json 事故的恢复工具

tools/recoverstate —— 修复被写坏的 state.json:
  1) token 先从下载目录的 .ehdl.json 清单里找
  2) 找不到的用 E-Hentai 的 ?next=<gid> 定位法去站点抓
     (该 URL 会返回以 gid 开头的 25 条列表,正则取 token,
     offset 0..5 轮着试)
  3) 抓不到的说明画廊在站点上已被删除

-dry 预演不写盘,写盘前先备份原文件。

配套回归测试:
  - internal/core/pushed_test.go   推送历史读写、任务删除后仍保留
  - internal/core/recover_e2e_test.go  恢复出的任务能真下载(71/71 页)
  - internal/eh/recover_verify_test.go 恢复出的 token 能解析画廊
  - app_test.go                    点「清除已完成」后灰标不消失
  - internal/core/bridge_test.go   /tasks 与 /pushed 的 token 校验

后两者需要能连 e-hentai.org,设 EH_TEST_PROXY 才跑。
This commit is contained in:
2026-10-25 00:16:03 +08:00
parent 1d7d261490
commit 2d3c322e34
15 changed files with 1738 additions and 68 deletions
+52 -1
View File
@@ -20,6 +20,7 @@ import (
type App struct {
ctx context.Context
store *core.Store
pushed *core.PushedStore
engine *core.Engine
bridge *core.Bridge
@@ -75,6 +76,18 @@ func statePath() string {
return filepath.Join(dir, core.AppName, "state.json")
}
// pushedPath 推送历史路径(和 state.json 同目录)
//
// 单独一个文件:state.json 里的 tasks 会被「清除已完成」清掉,
// 但「推送过」是历史事实,不该跟着任务一起消失。
func pushedPath() string {
dir, err := os.UserConfigDir()
if err != nil {
dir = "."
}
return filepath.Join(dir, core.AppName, "pushed.json")
}
// NewApp 建应用
func NewApp() *App {
return &App{logMu: make(chan struct{}, 1)}
@@ -86,6 +99,7 @@ func NewApp() *App {
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.store = core.NewStore(statePath())
a.pushed = core.NewPushedStore(pushedPath())
s := a.store.Settings()
if s.DownloadRoot == "" {
@@ -132,6 +146,7 @@ func (a *App) startup(ctx context.Context) {
wruntime.WindowShow(a.ctx)
},
)
a.bridge.SetPushedStore(a.pushed)
a.bridge.Start(s.BridgePort)
}
@@ -147,6 +162,10 @@ func (a *App) shutdown(ctx context.Context) {
a.store.Stop()
a.store.Flush()
}
if a.pushed != nil {
a.pushed.Stop()
a.pushed.Flush()
}
}
// sink 把引擎事件转成 Wails 事件
@@ -354,8 +373,16 @@ func (a *App) RemoveTask(gid, token string) []core.Task {
}
// ClearFinished 清除已完成(不动本地文件)
//
// 注意:任务从列表里删掉了,但「推送过」这个历史事实保留在 pushed.json 里,
// 所以油猴脚本的灰标不会因为清除任务而消失。
func (a *App) ClearFinished() map[string]interface{} {
n := a.store.ClearFinished()
n, gids := a.store.ClearFinishedWithGIDs()
if a.pushed != nil {
for _, gid := range gids {
a.pushed.MarkCompleted(gid)
}
}
a.emitLog("info", fmt.Sprintf("清除了 %d 个已完成任务(本地已下载文件未动)", n))
return map[string]interface{}{"removed": n, "tasks": a.store.Tasks()}
}
@@ -363,6 +390,30 @@ func (a *App) ClearFinished() map[string]interface{} {
// CountDone 已完成数
func (a *App) CountDone() int { return a.store.CountDone() }
// PushedHistoryResult 推送历史
type PushedHistoryResult struct {
Count int `json:"count"`
Items []core.PushedEntry `json:"items"`
}
// PushedHistory 取推送历史(推过的画廊,任务删了也在)
func (a *App) PushedHistory() PushedHistoryResult {
if a.pushed == nil {
return PushedHistoryResult{Items: []core.PushedEntry{}}
}
return PushedHistoryResult{Count: a.pushed.Count(), Items: a.pushed.Entries()}
}
// ClearPushedHistory 清空推送历史(灰标会全部消失)
func (a *App) ClearPushedHistory() map[string]interface{} {
if a.pushed != nil {
a.pushed.Clear()
a.pushed.Flush()
a.emitLog("warn", "已清空推送历史(列表页的灰标会消失)")
}
return map[string]interface{}{"count": 0}
}
// ClearAll 清空任务列表(不动本地文件)
func (a *App) ClearAll(onlySelected bool) map[string]interface{} {
scope := "全部"