first commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// 前端「构建」:本项目无框架、无打包步骤,纯静态文件直接拷贝。
|
||||
//
|
||||
// 之所以还留这个脚本:Wails 的 embed 指向 frontend/dist,
|
||||
// 需要一个入口把源码同步进去。顺带避免手工拷贝漏文件。
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FRONTEND = __dirname;
|
||||
const DIST = path.join(FRONTEND, 'dist');
|
||||
|
||||
// 需要原样拷贝的入口文件与目录(wailsjs 不用拷 —— 那是 wails build 自动生成的绑定)
|
||||
const ENTRIES = ['index.html', 'css', 'js'];
|
||||
|
||||
function copy() {
|
||||
fs.mkdirSync(DIST, { recursive: true });
|
||||
for (const name of ENTRIES) {
|
||||
const src = path.join(FRONTEND, name);
|
||||
if (!fs.existsSync(src)) continue;
|
||||
fs.rmSync(path.join(DIST, name), { recursive: true, force: true });
|
||||
fs.cpSync(src, path.join(DIST, name), { recursive: true });
|
||||
}
|
||||
console.log('[frontend] 资源已同步到 dist/');
|
||||
}
|
||||
|
||||
copy();
|
||||
@@ -0,0 +1,223 @@
|
||||
:root {
|
||||
--bg: #16181d;
|
||||
--bg2: #1c1f26;
|
||||
--bg3: #23272f;
|
||||
--line: #2e333d;
|
||||
--fg: #e6e8ec;
|
||||
--dim: #8b93a3;
|
||||
--accent: #4a9eff;
|
||||
--green: #3fb950;
|
||||
--amber: #d29922;
|
||||
--red: #f85149;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0; height: 100%;
|
||||
background: var(--bg); color: var(--fg);
|
||||
font: 13px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app { display: flex; flex-direction: column; height: 100%; }
|
||||
|
||||
/* ---------- 顶栏 ---------- */
|
||||
.bar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(180deg, var(--bg2), var(--bg));
|
||||
border-bottom: 1px solid var(--line);
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.bar .actions { -webkit-app-region: no-drag; display: flex; gap: 8px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.logo { font-size: 22px; }
|
||||
.bar h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.sub { margin: 0; font-size: 11px; color: var(--dim); }
|
||||
|
||||
/* ---------- 按钮 ---------- */
|
||||
.btn {
|
||||
background: var(--bg3); color: var(--fg);
|
||||
border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 6px 13px; font-size: 12px; cursor: pointer;
|
||||
transition: background .12s, border-color .12s;
|
||||
}
|
||||
.btn:hover:not(:disabled) { background: #2c313a; border-color: #3d4450; }
|
||||
.btn:active:not(:disabled) { transform: translateY(1px); }
|
||||
.btn:disabled { opacity: .4; cursor: default; }
|
||||
.btn.primary { background: var(--accent); border-color: var(--accent); color: #04121f; font-weight: 600; }
|
||||
.btn.primary:hover:not(:disabled) { background: #62acff; }
|
||||
.btn.danger { color: var(--red); }
|
||||
.btn.icon { padding: 6px 9px; }
|
||||
.mini {
|
||||
background: none; border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 5px; padding: 2px 8px; font-size: 11px; cursor: pointer;
|
||||
}
|
||||
.mini:hover { color: var(--fg); }
|
||||
|
||||
/* ---------- 统计条 ---------- */
|
||||
.stats {
|
||||
display: flex; align-items: center; gap: 18px;
|
||||
padding: 9px 16px; background: var(--bg2);
|
||||
border-bottom: 1px solid var(--line); font-size: 11px;
|
||||
}
|
||||
.stat { display: flex; align-items: baseline; gap: 5px; color: var(--dim); }
|
||||
.stat b { font-size: 14px; color: var(--fg); font-variant-numeric: tabular-nums; }
|
||||
.stat.queued b { color: var(--dim); }
|
||||
.stat.running b { color: var(--accent); }
|
||||
.stat.done b { color: var(--green); }
|
||||
.stat.failed b { color: var(--red); }
|
||||
.progress-wrap { display: flex; align-items: center; gap: 10px; margin-left: auto; }
|
||||
.pbar { width: 200px; height: 5px; background: var(--bg3); border-radius: 3px; overflow: hidden; }
|
||||
.pbar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width .25s; }
|
||||
|
||||
/* ---------- 任务表 ---------- */
|
||||
.main { flex: 1; overflow: auto; position: relative; }
|
||||
.tasks { width: 100%; border-collapse: collapse; }
|
||||
.tasks thead th {
|
||||
position: sticky; top: 0; z-index: 2;
|
||||
background: var(--bg2); text-align: left; font-weight: 500;
|
||||
color: var(--dim); font-size: 11px;
|
||||
padding: 8px 10px; border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.tasks td { padding: 8px 10px; border-bottom: 1px solid #21252c; vertical-align: middle; }
|
||||
.tasks tr:hover td { background: #1b1e25; }
|
||||
.c-sel { width: 30px; }
|
||||
.c-gid { width: 80px; font-variant-numeric: tabular-nums; color: var(--dim); }
|
||||
.c-prog { width: 190px; }
|
||||
.c-stat { width: 80px; }
|
||||
.c-act { width: 110px; text-align: right; white-space: nowrap; }
|
||||
|
||||
.title-cell { display: flex; flex-direction: column; gap: 2px; }
|
||||
.title-cell .t { font-size: 12px; }
|
||||
.title-cell .e { font-size: 10.5px; color: var(--red); }
|
||||
|
||||
.rowprog { display: flex; align-items: center; gap: 8px; }
|
||||
.rowprog .rb { flex: 1; height: 4px; background: var(--bg3); border-radius: 2px; overflow: hidden; }
|
||||
.rowprog .rb i { display: block; height: 100%; background: var(--accent); }
|
||||
.rowprog .rn { font-size: 10.5px; color: var(--dim); font-variant-numeric: tabular-nums; min-width: 62px; text-align: right; }
|
||||
|
||||
.badge { display: inline-block; padding: 2px 7px; border-radius: 20px; font-size: 10.5px; }
|
||||
.b-queued { background: #2a2f38; color: var(--dim); }
|
||||
.b-running { background: rgba(74,158,255,.15); color: var(--accent); }
|
||||
.b-done { background: rgba(63,185,80,.15); color: var(--green); }
|
||||
.b-failed { background: rgba(248,81,73,.15); color: var(--red); }
|
||||
.b-partial { background: rgba(210,153,34,.15); color: var(--amber); }
|
||||
|
||||
.empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.empty[hidden] { display: none; }
|
||||
.empty-inner { text-align: center; color: var(--dim); }
|
||||
.empty .big { font-size: 15px; color: var(--fg); margin: 0 0 6px; }
|
||||
.empty code, .hint code { background: var(--bg3); padding: 1px 5px; border-radius: 4px; font-size: 11px; }
|
||||
.dim { font-size: 11px; opacity: .7; }
|
||||
|
||||
/* ---------- 日志 ---------- */
|
||||
.logpane { border-top: 1px solid var(--line); background: var(--bg2); flex-shrink: 0; }
|
||||
.log-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 14px; font-size: 11px; color: var(--dim); cursor: pointer;
|
||||
}
|
||||
.log-head .spacer { flex: 1; }
|
||||
.log-count { font-variant-numeric: tabular-nums; }
|
||||
.log-body {
|
||||
height: 0; overflow-y: auto; padding: 0 14px;
|
||||
font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
transition: height .18s ease;
|
||||
}
|
||||
.logpane.open .log-body { height: 168px; padding: 4px 14px 10px; }
|
||||
.logpane.open .caret { transform: rotate(90deg); }
|
||||
.log-line { white-space: pre-wrap; word-break: break-all; }
|
||||
.log-line .t { color: #555d6b; margin-right: 7px; }
|
||||
.log-line.info { color: var(--dim); }
|
||||
.log-line.warn { color: var(--amber); }
|
||||
.log-line.error { color: var(--red); }
|
||||
.log-line.ok { color: var(--green); }
|
||||
|
||||
/* ---------- 弹窗 ---------- */
|
||||
.modal {
|
||||
position: fixed; inset: 0; z-index: 50;
|
||||
background: rgba(0,0,0,.55);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
.modal[hidden] { display: none; }
|
||||
.sheet {
|
||||
width: 540px; max-width: 92vw; max-height: 86vh; overflow: auto;
|
||||
background: var(--bg2); border: 1px solid var(--line);
|
||||
border-radius: 12px; padding: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.5);
|
||||
}
|
||||
.sheet.narrow { width: 400px; }
|
||||
.sheet h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.hint { margin: 0 0 12px; font-size: 11px; color: var(--dim); }
|
||||
#cf-body { font-size: 12px; color: var(--fg); line-height: 1.65; }
|
||||
.btn.danger:disabled { opacity: .35; }
|
||||
.sheet textarea {
|
||||
width: 100%; height: 190px; resize: vertical;
|
||||
background: var(--bg); color: var(--fg);
|
||||
border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 10px; font: 12px/1.6 ui-monospace, Menlo, monospace;
|
||||
}
|
||||
.sheet textarea:focus, .sheet input:focus, .sheet select:focus {
|
||||
outline: none; border-color: var(--accent);
|
||||
}
|
||||
.row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
|
||||
.sheet-foot { display: flex; gap: 8px; justify-content: flex-end; align-items: center; margin-top: 16px; }
|
||||
.sheet-foot .msg { flex: 1; font-size: 11px; color: var(--amber); }
|
||||
|
||||
.field { display: block; margin-bottom: 16px; }
|
||||
.field > span { display: block; margin-bottom: 6px; font-size: 12px; }
|
||||
.field small { display: block; margin-top: 5px; font-size: 10.5px; color: var(--dim); line-height: 1.5; }
|
||||
.field input[type=text], .field select {
|
||||
width: 100%; background: var(--bg); color: var(--fg);
|
||||
border: 1px solid var(--line); border-radius: var(--radius);
|
||||
padding: 7px 10px; font-size: 12px;
|
||||
}
|
||||
.field input[type=range] { width: 100%; accent-color: var(--accent); }
|
||||
.field .row input[type=text] { flex: 1; }
|
||||
|
||||
.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.presets button {
|
||||
background: var(--bg); color: var(--dim);
|
||||
border: 1px solid var(--line); border-radius: 20px;
|
||||
padding: 3px 11px; font-size: 11px; cursor: pointer;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
}
|
||||
.presets button:hover { color: var(--fg); border-color: var(--accent); }
|
||||
.presets button.on { color: var(--accent); border-color: var(--accent); }
|
||||
#dir-preview {
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
color: var(--green) !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
code { background: var(--bg3); padding: 1px 5px; border-radius: 4px; font-size: 10.5px; }
|
||||
/* ---------------- 站点域名管理 ---------------- */
|
||||
.site-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1.4fr 1fr auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.site-row .site-on {
|
||||
width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent);
|
||||
}
|
||||
.site-row input[type="text"] {
|
||||
width: 100%;
|
||||
background: var(--bg3); border: 1px solid var(--line);
|
||||
border-radius: 7px; color: var(--fg);
|
||||
padding: 6px 9px; font-size: 12px;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
}
|
||||
.site-row input[type="text"]:focus {
|
||||
outline: none; border-color: var(--accent);
|
||||
}
|
||||
.site-row .site-del {
|
||||
background: transparent; border: 1px solid var(--line);
|
||||
color: var(--dim); border-radius: 6px;
|
||||
width: 26px; height: 26px; cursor: pointer; font-size: 12px;
|
||||
}
|
||||
.site-row .site-del:hover { color: var(--red); border-color: var(--red); }
|
||||
@@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>4KHD Downloader</title>
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
|
||||
<header class="bar">
|
||||
<div class="brand">
|
||||
<span class="logo">🐳</span>
|
||||
<div>
|
||||
<h1>4KHD Downloader</h1>
|
||||
<p class="sub">4KHD 画廊批量下载 · 图片直链协议 · Wails</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="btn-import" class="btn">导入链接</button>
|
||||
<button id="btn-start" class="btn primary">开始下载</button>
|
||||
<button id="btn-stop" class="btn danger" disabled>停止</button>
|
||||
<button id="btn-clear" class="btn" title="清除已完成的任务列表项,并顺带删掉画廊目录里小于 10KB 的过小图片(占位图/损坏图)。正常图片不会动。">清除已完成</button>
|
||||
<button id="btn-delsel" class="btn danger" disabled title="删除勾选的任务。已下载的文件不会删除。">删除选中</button>
|
||||
<button id="btn-clearall" class="btn danger" title="清空整个任务列表。本地已下载的文件不会删除。">删除全部</button>
|
||||
<button id="btn-settings" class="btn icon" title="设置">⚙</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stats" id="stats">
|
||||
<div class="stat"><b id="s-total">0</b><span>全部</span></div>
|
||||
<div class="stat queued"><b id="s-queued">0</b><span>等待</span></div>
|
||||
<div class="stat running"><b id="s-running">0</b><span>进行</span></div>
|
||||
<div class="stat done"><b id="s-done">0</b><span>完成</span></div>
|
||||
<div class="stat failed"><b id="s-failed">0</b><span>失败</span></div>
|
||||
<div class="stat progress-wrap">
|
||||
<div class="pbar"><i id="s-bar"></i></div>
|
||||
<span id="s-text">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="main">
|
||||
<table class="tasks" id="tasks">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="c-sel"><input type="checkbox" id="sel-all" /></th>
|
||||
<th class="c-gid">名称</th>
|
||||
<th class="c-title">标题</th>
|
||||
<th class="c-prog">进度</th>
|
||||
<th class="c-stat">状态</th>
|
||||
<th class="c-act">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
<div class="empty" id="empty">
|
||||
<div class="empty-inner">
|
||||
<p class="big">还没有任务</p>
|
||||
<p>点「导入链接」,粘贴 <code>https://migmg.ssuu.uk/content/09/myua-sweetbox-vol52.html</code></p>
|
||||
<p class="dim">一行一个画廊,也支持逗号/制表符分隔。装了油猴脚本的话,浏览时点一下按钮更快。</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<section class="logpane" id="logpane">
|
||||
<div class="log-head" id="log-head">
|
||||
<span class="caret">▸</span>
|
||||
<span>日志</span>
|
||||
<span class="log-count" id="log-count"></span>
|
||||
<div class="spacer"></div>
|
||||
<button class="mini" id="btn-clearlog">清空</button>
|
||||
<button class="mini" id="btn-togglelog">展开</button>
|
||||
</div>
|
||||
<div class="log-body" id="log-body"></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 导入 -->
|
||||
<div class="modal" id="modal-import" hidden>
|
||||
<div class="sheet">
|
||||
<h2>导入画廊链接</h2>
|
||||
<p class="hint">支持完整链接 <code>https://站点域名/content/{分类}/{名字}.html</code>。每行一个,<code>/2</code>、<code>/3</code> 这类分页后缀会自动归一到第 1 页。</p>
|
||||
<textarea id="import-text" placeholder="https://migmg.ssuu.uk/content/09/myua-sweetbox-vol52.html https://migmg.ssuu.uk/content/11/another-gallery.html"></textarea>
|
||||
<div class="row">
|
||||
<input type="file" id="import-file" accept=".txt,text/plain" />
|
||||
</div>
|
||||
<div class="sheet-foot">
|
||||
<span class="msg" id="import-msg"></span>
|
||||
<button class="btn" id="btn-import-cancel">取消</button>
|
||||
<button class="btn primary" id="btn-import-ok">导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通用确认 -->
|
||||
<div class="modal" id="modal-confirm" hidden>
|
||||
<div class="sheet narrow">
|
||||
<h2 id="cf-title">确认</h2>
|
||||
<p class="hint" id="cf-body"></p>
|
||||
<div class="sheet-foot">
|
||||
<span class="msg" id="cf-msg"></span>
|
||||
<button class="btn" id="cf-cancel">取消</button>
|
||||
<button class="btn danger" id="cf-ok">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置 -->
|
||||
<div class="modal" id="modal-settings" hidden>
|
||||
<div class="sheet">
|
||||
<h2>设置</h2>
|
||||
|
||||
<label class="field">
|
||||
<span>站点域名</span>
|
||||
<div id="set-sites"></div>
|
||||
<div class="row" style="margin-top:6px">
|
||||
<button class="btn" id="btn-site-add" type="button">+ 添加域名</button>
|
||||
</div>
|
||||
<small>这类采集站随时换域名(实测 www.4khd.com / m.4khd.com 已全部 404)。
|
||||
换域名后在这里加一行,后端解析和油猴脚本的 @match 都会跟着更新,不用改代码。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>下载目录</span>
|
||||
<div class="row">
|
||||
<input type="text" id="set-dir" />
|
||||
<button class="btn" id="btn-pickdir">选择…</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>目录结构模板</span>
|
||||
<div class="row">
|
||||
<input type="text" id="set-dirtpl" placeholder="{title}" />
|
||||
</div>
|
||||
<div class="presets" id="dir-presets"></div>
|
||||
<small id="dir-preview"></small>
|
||||
<small>占位符:<code>{title} {name} {slug} {host} {category} {id}</code><br>
|
||||
换模板不会重下已完成的图 —— 引擎会先去老的「标题目录」里找。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>代理模式</span>
|
||||
<select id="set-proxymode">
|
||||
<option value="off">不使用代理(直连)</option>
|
||||
<option value="http">HTTP / HTTPS 代理</option>
|
||||
<option value="socks5">SOCKS5 代理</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>代理地址</span>
|
||||
<input type="text" id="set-proxy" placeholder="http://127.0.0.1:7890 或 socks5://127.0.0.1:1080" />
|
||||
<small>直连能通就不用填。不通的话填本机代理端口(mihomo 默认 7890)。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>画廊并发:<b id="set-gconc-v">5</b> 个</span>
|
||||
<input type="range" id="set-gconc" min="1" max="20" step="1" value="5" />
|
||||
<small>同时处理几个画廊。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>单画廊图片并发:<b id="set-pconc-v">8</b> 张</span>
|
||||
<input type="range" id="set-pconc" min="1" max="20" step="1" value="8" />
|
||||
<small>每个画廊内部同时下几张图。<br>
|
||||
总并发 = 画廊并发 × 图并发,默认 <b id="set-max">40</b> 张同时下载。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>请求间隔:<b id="set-interval-v">0</b> 秒</span>
|
||||
<input type="range" id="set-interval" min="0" max="50" step="1" value="0" />
|
||||
<small>所有请求之间的最小间隔(全局生效,含翻页抓取与图片下载)。0 = 不限流。<br>
|
||||
图片走 Jetpack Photon CDN,打快了会吃 429,遇到的话调到 1~2 秒。</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>请求超时:<b id="set-to-v">30</b> 秒</span>
|
||||
<input type="range" id="set-to" min="10" max="120" step="5" value="30" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>油猴推送端口</span>
|
||||
<input type="text" id="set-bridgeport" placeholder="37778" />
|
||||
<small>本地桥口端口,供油猴脚本推送画廊用。改这里的话,油猴脚本里的 PORT 也要同步改。<br>
|
||||
启动日志里有「本地桥口已就绪 http://127.0.0.1:端口」那行,可用来确认实际生效端口。</small>
|
||||
</label>
|
||||
|
||||
<div class="sheet-foot">
|
||||
<span class="msg" id="set-msg"></span>
|
||||
<button class="btn" id="btn-settings-cancel">取消</button>
|
||||
<button class="btn primary" id="btn-settings-ok">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="wailsjs/go/main/App.js"></script>
|
||||
<script src="wailsjs/runtime/runtime.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
// Wails API 适配层
|
||||
//
|
||||
// 前端业务代码只用 window.eh.*,这里把它接到 Wails 的 Go bindings 上。
|
||||
// 这样以后即便换框架,也只要改这一个文件。
|
||||
//
|
||||
// 命名从 eh.* 改成 hd4k.* —— 站点换了,标识也跟着换,避免误以为还是 EH。
|
||||
|
||||
const go = () => window.go && window.go.main && window.go.main.App;
|
||||
|
||||
function requireGo() {
|
||||
const g = go();
|
||||
if (!g) throw new Error('Wails 后端未就绪');
|
||||
return g;
|
||||
}
|
||||
|
||||
window.hd4k = {
|
||||
// ---- 设置 ----
|
||||
getSettings: async () => requireGo().GetSettings(),
|
||||
setSettings: async (patch) => requireGo().SetSettings(patch),
|
||||
previewDir: async (tpl) => requireGo().PreviewDir(tpl),
|
||||
placeholders: async () => requireGo().Placeholders(),
|
||||
sites: async () => requireGo().Sites(),
|
||||
|
||||
// ---- 任务 ----
|
||||
listTasks: async () => requireGo().ListTasks(),
|
||||
addTasks: async (text) => requireGo().AddTasks(text),
|
||||
removeTask: async (id) => requireGo().RemoveTask(id),
|
||||
clearFinished: async () => requireGo().ClearFinished(),
|
||||
countDone: async () => requireGo().CountDone(),
|
||||
clearAll: async (onlySelected) => requireGo().ClearAll(!!onlySelected),
|
||||
requeueFailed: async () => requireGo().RequeueFailed(),
|
||||
selectAll: async (ids) => requireGo().SelectAll(ids),
|
||||
|
||||
// ---- 引擎 ----
|
||||
status: async () => requireGo().Status(),
|
||||
engineInfo: async () => requireGo().EngineInfo(),
|
||||
start: async (onlyIds) => requireGo().Start(onlyIds || []),
|
||||
stop: async () => requireGo().Stop(),
|
||||
|
||||
// ---- 扫描下载目录(用 .hd4k.json 清单补全丢失的任务信息)----
|
||||
scanDownloadDir: async () => requireGo().ScanDownloadDir(),
|
||||
|
||||
// ---- 系统 ----
|
||||
pickDir: async () => requireGo().PickDir(),
|
||||
exportFailed: async (id) => {
|
||||
const path = await requireGo().ExportFailed(id);
|
||||
return path ? { ok: true, filePath: path } : { ok: false };
|
||||
},
|
||||
openPath: async (p) => requireGo().OpenPath(p),
|
||||
showItem: async (p) => requireGo().ShowItem(p),
|
||||
|
||||
// ---- 事件 ----
|
||||
onTaskUpdate: (cb) => window.runtime.EventsOn('task:update', cb),
|
||||
onLog: (cb) => window.runtime.EventsOn('log', cb),
|
||||
onDone: (cb) => window.runtime.EventsOn('engine:done', cb),
|
||||
};
|
||||
|
||||
// 兼容别名:renderer.js 里的业务代码统一用 window.eh.*
|
||||
window.eh = window.hd4k;
|
||||
@@ -0,0 +1,543 @@
|
||||
'use strict';
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let tasks = [];
|
||||
let running = false;
|
||||
let lastLogSeq = 0;
|
||||
const logBuf = [];
|
||||
|
||||
// ---------------- 渲染 ----------------
|
||||
|
||||
const STATUS_TEXT = {
|
||||
queued: '等待', running: '准备中', parsing: '解析中', downloading: '下载中',
|
||||
done: '完成', failed: '失败', partial: '部分失败',
|
||||
};
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
function render() {
|
||||
const tb = $('tbody');
|
||||
if (!tasks.length) {
|
||||
tb.innerHTML = '';
|
||||
$('empty').hidden = false;
|
||||
$('tasks').style.display = 'none';
|
||||
} else {
|
||||
$('empty').hidden = true;
|
||||
$('tasks').style.display = '';
|
||||
tb.innerHTML = tasks.map(rowHtml).join('');
|
||||
}
|
||||
renderStats();
|
||||
renderSelState();
|
||||
}
|
||||
|
||||
// 「删除选中」按钮的可用状态跟着勾选走
|
||||
function renderSelState() {
|
||||
const n = tasks.filter((t) => t.selected).length;
|
||||
const b = $('btn-delsel');
|
||||
b.disabled = n === 0;
|
||||
b.textContent = n ? `删除选中 (${n})` : '删除选中';
|
||||
$('btn-clearall').disabled = tasks.length === 0;
|
||||
}
|
||||
|
||||
function rowHtml(t) {
|
||||
// 4KHD 的 total 是**图片总数**(不是 EH 那种 HTML 分页数)
|
||||
const total = t.total || 0;
|
||||
|
||||
// 解析阶段:显示「已解析 N/M 张」,而不是 0/?(否则看着像卡死)
|
||||
const parsing = t.status === 'parsing';
|
||||
let progCell;
|
||||
if (parsing) {
|
||||
const got = Number((t.pages || {})['_parsing'] || 0);
|
||||
const pct = total ? Math.round((got / total) * 100) : 0;
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">已解析 ${got}${total ? '/' + total : ''} 张</span>
|
||||
</div>`;
|
||||
} else {
|
||||
const pct = total ? Math.round((t.done / total) * 100) : (t.status === 'done' ? 100 : 0);
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">${t.done}/${total || '?'}${t.failed ? ` (✗${t.failed})` : ''}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const title = t.title || t.raw || '(未解析)';
|
||||
const err = t.error
|
||||
? `<span class="e">${esc(t.error)}</span>`
|
||||
: (t.failedList && t.failedList.length
|
||||
? `<span class="e">${t.failedList.length} 张失败:${esc(t.failedList[0].error || '')}</span>`
|
||||
: '');
|
||||
const acts = [];
|
||||
if (t.failedList && t.failedList.length) {
|
||||
acts.push(`<button class="mini" data-act="export" data-id="${t.id}">导出失败</button>`);
|
||||
}
|
||||
acts.push(`<button class="mini" data-act="del" data-id="${t.id}">删除</button>`);
|
||||
|
||||
// 「ID」列展示 slug(去掉 .html),比完整 URL 好认
|
||||
const slug = t.slug || t.url || '';
|
||||
|
||||
return `<tr data-id="${t.id}">
|
||||
<td class="c-sel"><input type="checkbox" class="sel" data-id="${t.id}" ${t.selected ? 'checked' : ''} /></td>
|
||||
<td class="c-gid">${esc(slug)}</td>
|
||||
<td><div class="title-cell"><span class="t">${esc(title)}</span>${err}</div></td>
|
||||
<td class="c-prog">${progCell}</td>
|
||||
<td class="c-stat"><span class="badge ${'b-' + (t.status || 'queued')}">${STATUS_TEXT[t.status] || t.status}</span></td>
|
||||
<td class="c-act">${acts.join(' ')}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const c = { queued: 0, running: 0, done: 0, failed: 0 };
|
||||
let dn = 0, tt = 0;
|
||||
for (const t of tasks) {
|
||||
if (t.status === 'queued') c.queued++;
|
||||
else if (t.status === 'running' || t.status === 'parsing' || t.status === 'downloading') c.running++;
|
||||
else if (t.status === 'done') c.done++;
|
||||
else if (t.status === 'failed' || t.status === 'partial') c.failed++;
|
||||
// 解析阶段的任务还没进入下载,done 不计入总进度
|
||||
if (t.status !== 'parsing') {
|
||||
dn += t.done || 0;
|
||||
tt += t.total || 0;
|
||||
}
|
||||
}
|
||||
$('s-total').textContent = tasks.length;
|
||||
$('s-queued').textContent = c.queued;
|
||||
$('s-running').textContent = c.running;
|
||||
$('s-done').textContent = c.done;
|
||||
$('s-failed').textContent = c.failed;
|
||||
const pct = tt ? Math.round((dn / tt) * 100) : 0;
|
||||
$('s-bar').style.width = pct + '%';
|
||||
$('s-text').textContent = tt ? `${dn}/${tt} 张 · ${pct}%` : '—';
|
||||
}
|
||||
|
||||
function applyTask(t) {
|
||||
const i = tasks.findIndex((x) => x.id === t.id);
|
||||
if (i >= 0) tasks[i] = { ...tasks[i], ...t };
|
||||
else tasks.unshift(t);
|
||||
}
|
||||
|
||||
// ---------------- 日志 ----------------
|
||||
|
||||
function pushLog(rec) {
|
||||
if (rec.seq && rec.seq <= lastLogSeq) return;
|
||||
if (rec.seq) lastLogSeq = rec.seq;
|
||||
logBuf.push(rec);
|
||||
if (logBuf.length > 2000) logBuf.shift();
|
||||
const d = new Date(rec.at);
|
||||
const ts = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
||||
const el = document.createElement('div');
|
||||
el.className = 'log-line ' + (rec.level || 'info');
|
||||
el.innerHTML = `<span class="t">${ts}</span>${esc(rec.msg)}`;
|
||||
const body = $('log-body');
|
||||
const stick = body.scrollHeight - body.scrollTop - body.clientHeight < 40;
|
||||
body.appendChild(el);
|
||||
if (stick) body.scrollTop = body.scrollHeight;
|
||||
$('log-count').textContent = `(${logBuf.length})`;
|
||||
}
|
||||
|
||||
// ---------------- 弹窗 ----------------
|
||||
|
||||
function openModal(id) { $(id).hidden = false; }
|
||||
function closeModal(id) { $(id).hidden = true; }
|
||||
|
||||
// ---------------- 设置 ----------------
|
||||
|
||||
let settings = {};
|
||||
|
||||
async function loadSettings() {
|
||||
settings = await window.eh.getSettings();
|
||||
$('set-dir').value = settings.downloadRoot || '';
|
||||
$('set-proxy').value = settings.proxy || '';
|
||||
$('set-proxymode').value = settings.proxyMode || 'off';
|
||||
$('set-gconc').value = settings.galleryConcurrency || 5;
|
||||
$('set-gconc-v').textContent = settings.galleryConcurrency || 5;
|
||||
$('set-pconc').value = settings.pageConcurrency || 8;
|
||||
$('set-pconc-v').textContent = settings.pageConcurrency || 8;
|
||||
$('set-interval').value = Math.round((settings.requestIntervalMs || 0) / 1000);
|
||||
$('set-interval-v').textContent = Math.round((settings.requestIntervalMs || 0) / 1000);
|
||||
$('set-to').value = Math.round((settings.timeout || 30000) / 1000);
|
||||
$('set-to-v').textContent = Math.round((settings.timeout || 30000) / 1000);
|
||||
$('set-bridgeport').value = String(settings.bridgePort || 37778);
|
||||
$('set-dirtpl').value = settings.dirTemplate || '{title}';
|
||||
renderSites(settings.sites || []);
|
||||
updateMaxConcurrent();
|
||||
renderPresets();
|
||||
refreshPreview();
|
||||
}
|
||||
|
||||
function updateMaxConcurrent() {
|
||||
const g = parseInt($('set-gconc').value, 10) || 5;
|
||||
const p = parseInt($('set-pconc').value, 10) || 8;
|
||||
$('set-max').textContent = g * p;
|
||||
}
|
||||
|
||||
// 4KHD 没有 EH 那套 artist/group/tags 标签体系,站点真实提供的只有
|
||||
// 标题、slug、域名、分类数字 —— 所以预设也只剩这些。
|
||||
const DIR_PRESETS = [
|
||||
['{title}', '仅标题'],
|
||||
['{name}', '路径名'],
|
||||
['{category}/{title}', '分类/标题'],
|
||||
['{host}/{title}', '域名/标题'],
|
||||
['{category}/{name}', '分类/路径名'],
|
||||
['{name} - {title}', '路径名 - 标题'],
|
||||
];
|
||||
|
||||
function renderPresets() {
|
||||
const box = $('dir-presets');
|
||||
const cur = $('set-dirtpl').value;
|
||||
box.innerHTML = '';
|
||||
for (const [tpl, label] of DIR_PRESETS) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = label;
|
||||
b.title = tpl;
|
||||
if (tpl === cur) b.classList.add('on');
|
||||
b.onclick = () => {
|
||||
$('set-dirtpl').value = tpl;
|
||||
renderPresets();
|
||||
refreshPreview();
|
||||
};
|
||||
box.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPreview() {
|
||||
const tpl = $('set-dirtpl').value;
|
||||
const r = await window.eh.previewDir(tpl);
|
||||
$('dir-preview').textContent = r.ok ? `示例结果:${r.path}` : `模板有误:${r.error}`;
|
||||
}
|
||||
|
||||
// ---------------- 事件 ----------------
|
||||
|
||||
function bind() {
|
||||
$('btn-import').onclick = () => { $('import-text').value = ''; $('import-msg').textContent = ''; openModal('modal-import'); };
|
||||
$('btn-import-cancel').onclick = () => closeModal('modal-import');
|
||||
$('btn-import-ok').onclick = doImport;
|
||||
|
||||
$('import-file').onchange = async (e) => {
|
||||
const f = e.target.files[0];
|
||||
if (!f) return;
|
||||
$('import-text').value = await f.text();
|
||||
};
|
||||
|
||||
$('btn-start').onclick = doStart;
|
||||
$('btn-stop').onclick = async () => {
|
||||
await window.eh.stop();
|
||||
setRunning(false);
|
||||
};
|
||||
$('btn-clear').onclick = doClearFinished;
|
||||
|
||||
$('btn-settings').onclick = async () => { await loadSettings(); openModal('modal-settings'); };
|
||||
$('btn-settings-cancel').onclick = () => closeModal('modal-settings');
|
||||
$('btn-settings-ok').onclick = saveSettings;
|
||||
$('btn-pickdir').onclick = async () => {
|
||||
const d = await window.eh.pickDir();
|
||||
if (d) $('set-dir').value = d;
|
||||
};
|
||||
$('btn-site-add').onclick = () => {
|
||||
const box = $('set-sites');
|
||||
box.appendChild(siteRow());
|
||||
box.lastChild.querySelector('.site-host').focus();
|
||||
};
|
||||
|
||||
$('set-gconc').oninput = (e) => {
|
||||
$('set-gconc-v').textContent = e.target.value;
|
||||
updateMaxConcurrent();
|
||||
};
|
||||
$('set-pconc').oninput = (e) => {
|
||||
$('set-pconc-v').textContent = e.target.value;
|
||||
updateMaxConcurrent();
|
||||
};
|
||||
$('set-interval').oninput = (e) => { $('set-interval-v').textContent = e.target.value; };
|
||||
$('set-to').oninput = (e) => { $('set-to-v').textContent = e.target.value; };
|
||||
$('set-dirtpl').oninput = () => { renderPresets(); refreshPreview(); };
|
||||
|
||||
$('sel-all').onchange = async (e) => {
|
||||
for (const cb of document.querySelectorAll('.sel')) cb.checked = e.target.checked;
|
||||
for (const t of tasks) t.selected = e.target.checked;
|
||||
renderSelState();
|
||||
await window.eh.selectAll(
|
||||
e.target.checked ? tasks.map((t) => t.id) : []
|
||||
);
|
||||
};
|
||||
|
||||
// 单行勾选:同步到主进程并刷新「删除选中」按钮
|
||||
$('tbody').addEventListener('change', async (e) => {
|
||||
const cb = e.target.closest('input.sel');
|
||||
if (!cb) return;
|
||||
const t = tasks.find((x) => x.id === cb.dataset.id);
|
||||
if (t) t.selected = cb.checked;
|
||||
renderSelState();
|
||||
await window.eh.selectAll(tasks.filter((x) => x.selected).map((x) => x.id));
|
||||
});
|
||||
|
||||
$('btn-clearall').onclick = () => doClearAll(false);
|
||||
$('btn-delsel').onclick = () => doClearAll(true);
|
||||
|
||||
$('tbody').onclick = async (e) => {
|
||||
const btn = e.target.closest('button[data-act]');
|
||||
if (!btn) return;
|
||||
const { act, id } = btn.dataset;
|
||||
if (act === 'del') {
|
||||
await window.eh.removeTask(id);
|
||||
tasks = tasks.filter((t) => t.id !== id);
|
||||
render();
|
||||
} else if (act === 'export') {
|
||||
const r = await window.eh.exportFailed(id);
|
||||
if (r.ok) pushLog({ level: 'ok', msg: `失败清单已导出:${r.filePath}`, at: Date.now() });
|
||||
}
|
||||
};
|
||||
|
||||
$('log-head').onclick = (e) => {
|
||||
if (e.target.closest('button')) return;
|
||||
toggleLog();
|
||||
};
|
||||
$('btn-togglelog').onclick = toggleLog;
|
||||
$('btn-clearlog').onclick = () => {
|
||||
logBuf.length = 0; lastLogSeq = 0;
|
||||
$('log-body').innerHTML = '';
|
||||
$('log-count').textContent = '';
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (confirmCancelHook) { confirmCancelHook(); return; }
|
||||
closeModal('modal-import');
|
||||
closeModal('modal-settings');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLog() {
|
||||
const p = $('logpane');
|
||||
p.classList.toggle('open');
|
||||
$('btn-togglelog').textContent = p.classList.contains('open') ? '收起' : '展开';
|
||||
}
|
||||
|
||||
async function doImport() {
|
||||
const text = $('import-text').value.trim();
|
||||
if (!text) { $('import-msg').textContent = '先粘贴点内容'; return; }
|
||||
const r = await window.eh.addTasks(text);
|
||||
if (r.warning) $('import-msg').textContent = r.warning;
|
||||
tasks = await window.eh.listTasks();
|
||||
render();
|
||||
if (!r.warning) {
|
||||
pushLog({ level: 'ok', msg: `已导入 ${r.added} 个画廊`, at: Date.now() });
|
||||
closeModal('modal-import');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const patch = {
|
||||
downloadRoot: $('set-dir').value.trim(),
|
||||
proxy: $('set-proxy').value.trim(),
|
||||
proxyMode: $('set-proxymode').value,
|
||||
galleryConcurrency: parseInt($('set-gconc').value, 10),
|
||||
pageConcurrency: parseInt($('set-pconc').value, 10),
|
||||
requestIntervalMs: parseInt($('set-interval').value, 10) * 1000,
|
||||
timeout: parseInt($('set-to').value, 10) * 1000,
|
||||
bridgePort: parseInt($('set-bridgeport').value, 10) || 37778,
|
||||
dirTemplate: $('set-dirtpl').value.trim() || '{title}',
|
||||
sites: collectSites(),
|
||||
};
|
||||
await window.eh.setSettings(patch);
|
||||
$('set-msg').textContent = '';
|
||||
closeModal('modal-settings');
|
||||
const max = patch.galleryConcurrency * patch.pageConcurrency;
|
||||
pushLog({
|
||||
level: 'ok',
|
||||
msg: `设置已保存 · 画廊并发 ${patch.galleryConcurrency} × 图并发 ${patch.pageConcurrency}(最多 ${max} 张同时下载)` +
|
||||
(patch.requestIntervalMs ? ` · 请求间隔 ${patch.requestIntervalMs / 1000} 秒` : '') +
|
||||
(patch.sites.length ? ` · 站点 ${patch.sites.map((s) => s.host).join(', ')}` : ''),
|
||||
at: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async function doStart() {
|
||||
const sel = [...document.querySelectorAll('.sel')].filter((c) => c.checked);
|
||||
const ids = sel.map((c) => c.dataset.id);
|
||||
const r = await window.eh.start(ids);
|
||||
if (!r.started) {
|
||||
pushLog({ level: 'warn', msg: r.reason, at: Date.now() });
|
||||
return;
|
||||
}
|
||||
setRunning(true);
|
||||
}
|
||||
|
||||
// ---------------- 站点域名管理 ----------------
|
||||
//
|
||||
// 这类采集站随时换域名(实测 www.4khd.com / m.4khd.com 已全 404,
|
||||
// 只有 migmg.ssuu.uk 还活着)。域名做成设置项:站点挂了加一行即可,
|
||||
// 后端解析规则和油猴的 @match 都不用动。
|
||||
function renderSites(sites) {
|
||||
const box = $('set-sites');
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
sites.forEach((s, i) => box.appendChild(siteRow(s, i)));
|
||||
}
|
||||
|
||||
function siteRow(s = { host: '', label: '', enabled: true }, idx = 0) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'site-row';
|
||||
row.innerHTML = `
|
||||
<input type="checkbox" class="site-on" ${s.enabled ? 'checked' : ''} title="启用该站点" />
|
||||
<input type="text" class="site-host" placeholder="migmg.ssuu.uk" value="${esc(s.host || '')}" />
|
||||
<input type="text" class="site-label" placeholder="显示名(可选)" value="${esc(s.label || '')}" />
|
||||
<button type="button" class="mini site-del" title="删除">✕</button>`;
|
||||
row.querySelector('.site-del').onclick = () => { row.remove(); };
|
||||
return row;
|
||||
}
|
||||
|
||||
function collectSites() {
|
||||
const out = [];
|
||||
document.querySelectorAll('#set-sites .site-row').forEach((row) => {
|
||||
const host = row.querySelector('.site-host').value.trim();
|
||||
if (!host) return;
|
||||
out.push({
|
||||
host,
|
||||
label: row.querySelector('.site-label').value.trim(),
|
||||
enabled: row.querySelector('.site-on').checked,
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// 清除已完成:两步确认,避免误点。
|
||||
// 除了清任务列表,还会顺带删掉画廊目录里 <10KB 的过小图片(占位图/损坏图)。
|
||||
let clearArmed = false;
|
||||
let clearTimer = null;
|
||||
|
||||
async function doClearFinished() {
|
||||
const btn = $('btn-clear');
|
||||
const doneCount = tasks.filter((t) => t.status === 'done').length;
|
||||
|
||||
if (!clearArmed) {
|
||||
clearArmed = true;
|
||||
// 即使没有已完成任务也允许确认 —— 没有任务时这一步只做「清理小图」
|
||||
btn.textContent = doneCount
|
||||
? `确认清除 ${doneCount} 项并清理小图?`
|
||||
: '确认清理画廊里的小图?';
|
||||
btn.classList.add('primary');
|
||||
clearTimer = setTimeout(disarmClear, 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
disarmClear();
|
||||
const r = await window.eh.clearFinished();
|
||||
tasks = r.tasks;
|
||||
render();
|
||||
|
||||
let msg = `已清除 ${r.removed} 个已完成任务`;
|
||||
if (r.sweptImages > 0) {
|
||||
msg += ` · 删除 ${r.sweptImages} 张过小图片(释放 ${r.sweptFreedKB} KB)`;
|
||||
} else {
|
||||
msg += ' · 没有发现过小的图片';
|
||||
}
|
||||
pushLog({ level: 'ok', msg, at: Date.now() });
|
||||
}
|
||||
|
||||
function disarmClear() {
|
||||
clearArmed = false;
|
||||
if (clearTimer) { clearTimeout(clearTimer); clearTimer = null; }
|
||||
const btn = $('btn-clear');
|
||||
btn.textContent = '清除已完成';
|
||||
btn.classList.remove('primary');
|
||||
}
|
||||
|
||||
// ---------------- 删除任务(列表项,本地文件不动) ----------------
|
||||
|
||||
/** 通用确认弹窗,resolve(true)=确认 false=取消/ESC */
|
||||
let confirmCancelHook = null;
|
||||
|
||||
function confirmDialog(title, body, okText = '确认') {
|
||||
return new Promise((resolve) => {
|
||||
$('cf-title').textContent = title;
|
||||
$('cf-body').textContent = body;
|
||||
$('cf-msg').textContent = '';
|
||||
const ok = $('cf-ok');
|
||||
ok.textContent = okText;
|
||||
openModal('modal-confirm');
|
||||
|
||||
const finish = (v) => {
|
||||
ok.onclick = null;
|
||||
$('cf-cancel').onclick = null;
|
||||
confirmCancelHook = null;
|
||||
closeModal('modal-confirm');
|
||||
resolve(v);
|
||||
};
|
||||
|
||||
ok.onclick = () => finish(true);
|
||||
$('cf-cancel').onclick = () => finish(false);
|
||||
confirmCancelHook = () => finish(false);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {boolean} onlySelected true=只删勾选的,false=清空全部 */
|
||||
async function doClearAll(onlySelected) {
|
||||
const n = onlySelected ? tasks.filter((t) => t.selected).length : tasks.length;
|
||||
|
||||
if (!n) {
|
||||
pushLog({
|
||||
level: 'warn',
|
||||
msg: onlySelected ? '没有勾选任何任务' : '任务列表已经是空的',
|
||||
at: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await confirmDialog(
|
||||
onlySelected ? `确认删除选中的 ${n} 个任务?` : `确认删除全部 ${n} 个任务?`,
|
||||
`将从任务列表里移除${onlySelected ? `这 ${n} 个` : `全部 ${n} 个`}任务。` +
|
||||
`\n\n⚠️ 只删列表项,本地已经下载的图片不会受到任何影响。`,
|
||||
'删除'
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
const r = await window.eh.clearAll(onlySelected);
|
||||
tasks = r.tasks;
|
||||
render();
|
||||
pushLog({
|
||||
level: 'warn',
|
||||
msg: `已删除 ${r.removed} 个任务 · 本地已下载的文件一个没动`,
|
||||
at: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function setRunning(v) {
|
||||
running = v;
|
||||
$('btn-start').disabled = v;
|
||||
$('btn-stop').disabled = !v;
|
||||
}
|
||||
|
||||
// ---------------- 启动 ----------------
|
||||
|
||||
async function init() {
|
||||
bind();
|
||||
await loadSettings();
|
||||
tasks = await window.eh.listTasks();
|
||||
render();
|
||||
|
||||
const st = await window.eh.status();
|
||||
setRunning(st.running);
|
||||
|
||||
window.eh.onTaskUpdate((t) => {
|
||||
applyTask(t);
|
||||
render();
|
||||
if (t.status === 'running' || t.status === 'downloading') setRunning(true);
|
||||
});
|
||||
|
||||
window.eh.onLog(pushLog);
|
||||
|
||||
window.eh.onDone(() => {
|
||||
setRunning(false);
|
||||
pushLog({ level: 'ok', msg: '全部任务结束', at: Date.now() });
|
||||
});
|
||||
|
||||
pushLog({ level: 'info', msg: `就绪 · 共 ${tasks.length} 个任务`, at: Date.now() });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "hd4k-downloader-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Static assets sync script for the Wails frontend (no bundler)",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev": "node build.js --watch"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user