|
|
@@ -1,361 +1,292 @@
|
|
|
/* ============================== src/page-id.js ==============================
|
|
|
- * ID提取页(设计稿 3:1 / 16:23 / 16:319 / 16:706)。
|
|
|
+ * ID提取页 —— **三段式**(v8 / 2026-10-17)。
|
|
|
*
|
|
|
- * 结构:当前脚本提示 → 「脚本选择」(**单选**)→ 单选说明 → 4 张脚本卡
|
|
|
- * → 滚动指引(指向下方功能设置)→ 「功能设置」→ 运行约束提示(仅运行时)。
|
|
|
+ * ① 取材 从哪儿找人 —— 可累加的取材清单(每条自带起点与深度)
|
|
|
+ * ② 只要这样的账号 —— 过滤条件(所有取材共用)
|
|
|
+ * ③ 输出 上限 / 去重 / 文件
|
|
|
*
|
|
|
- * ⚠️ 本页的脚本卡是**单选**,不是多选。
|
|
|
- * 设计稿写得很明确:「单选:同一时间仅可运行一个脚本」。
|
|
|
- * 互斥逻辑放在页面里(Pages.toggleIdScript),因为它是这一页的业务规则,
|
|
|
- * 放组件里会让别的页面被迫接受同一条规则。
|
|
|
+ * ## 为什么不再是"四张脚本卡选一个"
|
|
|
+ * v7 及以前是四张卡单选。那四张卡其实是「起点(人从哪来)」×「深度(要不要再展开
|
|
|
+ * 一层)」的 4 个**不完整组合**:各缺一块、各有别人没有的一块,合起来才完整。
|
|
|
+ * 于是「同一时间仅可运行一个脚本」这条约束本身就在跟用户较劲 ——
|
|
|
+ * 想同时抓"关键词视频的作者"和"文本里这批 uid 的粉丝"时,它直接不允许。
|
|
|
+ *
|
|
|
+ * 现在的模型:
|
|
|
+ * · **五个起点**(指定账号 / 搜索结果 / 某个视频的评论区 / 文本文件 / 当前屏幕)
|
|
|
+ * · **一个深度选项**(不展开 / 再取粉丝 / 再取关注),最多 2 层
|
|
|
+ * · 取材可加多条,**按顺序串行执行**(真机只有一个前台,没有并发可言),
|
|
|
+ * 共用同一份去重集合与同一个输出文件
|
|
|
+ *
|
|
|
+ * ## 每个控件都有唯一含义
|
|
|
+ * 选了什么起点才出现什么输入 —— 不会再有"勾上却无所指"的开关。
|
|
|
+ * 这是 v7 那次"清语义"留下来的规矩,v8 继续守。
|
|
|
*/
|
|
|
|
|
|
var Pages = Pages || {};
|
|
|
|
|
|
/**
|
|
|
- * 各页脚本清单的集中注册表。
|
|
|
- * main.js 需要把「tab + scriptKey」翻译成界面上显示的中文名
|
|
|
- * (运行状态条、拦截提示、导出配置的日志都要用它)。
|
|
|
- * 让每个页面文件自己注册,比在 main.js 里再维护一份映射表更难漏。
|
|
|
+ * 各页脚本清单的集中注册表(main.js 用 key → 中文名翻译)。
|
|
|
+ * ID提取已经没有"脚本"这个概念,但仍然注册一个占位条目:
|
|
|
+ * 导出配置、运行日志、拦截提示里都会把 key 翻成中文名,
|
|
|
+ * 让那套通用机制不必为 id 开特例。
|
|
|
*/
|
|
|
Pages.SCRIPTS_OF = {};
|
|
|
-
|
|
|
Pages.registerScripts = function (tab, list) {
|
|
|
Pages.SCRIPTS_OF[tab] = list;
|
|
|
};
|
|
|
|
|
|
-/* 四张脚本卡的文案与摘要。摘要只在折叠态显示,展开后由具体设置行替代。
|
|
|
- * v7 顺手把 s2 的摘要从「评论裂变」改成「评论者」——
|
|
|
- * "裂变"这个词在脚本二里其实只表示"再取评论者的粉丝",
|
|
|
- * 而它在脚本三里又是另一种意思,同一个词两种含义最容易读糊。 */
|
|
|
-Pages.ID_SCRIPTS = [
|
|
|
- { key: "s1", name: "脚本一 · 粉丝页提 ID" },
|
|
|
- { key: "s2", name: "脚本二 · 评论页提 ID", summary: "评论者" },
|
|
|
- { key: "s3", name: "脚本三 · 文本 UID 裂变提 ID", summary: "文本 UID" },
|
|
|
- { key: "s4", name: "脚本四 · 关键词视频提 ID", summary: "关键词视频" }
|
|
|
-];
|
|
|
-
|
|
|
+Pages.ID_SCRIPTS = [{ key: "id", name: "ID提取" }];
|
|
|
Pages.registerScripts("id", Pages.ID_SCRIPTS);
|
|
|
|
|
|
-/** 取当前选中的脚本 key(未选返回空串)。 */
|
|
|
-Pages.checkedIdScript = function () {
|
|
|
- for (var i = 0; i < Pages.ID_SCRIPTS.length; i++) {
|
|
|
- var key = Pages.ID_SCRIPTS[i].key;
|
|
|
- if (Store.getBool("tabs.id.scripts." + key + ".checked")) { return key; }
|
|
|
- }
|
|
|
- return "";
|
|
|
-};
|
|
|
+/* ============================================================ ① 取材 */
|
|
|
|
|
|
-/** 取某个脚本的显示名,用于「当前脚本」提示与运行状态条。 */
|
|
|
-Pages.scriptName = function (tab, key) {
|
|
|
- var list = Pages.SCRIPTS_OF[tab] || [];
|
|
|
- for (var i = 0; i < list.length; i++) {
|
|
|
- if (list[i].key === key) { return list[i].name; }
|
|
|
- }
|
|
|
- /* 找不到就回退成 key 本身:显示 "s2" 也比显示 "undefined" 强 */
|
|
|
- return key;
|
|
|
-};
|
|
|
+/** 候选文案数组(下拉要的是纯文案,顺序与 Sources 里的表一致)。 */
|
|
|
+function labelsOf(table) {
|
|
|
+ var out = [];
|
|
|
+ for (var i = 0; i < table.length; i++) { out.push(table[i].label); }
|
|
|
+ return out;
|
|
|
+}
|
|
|
|
|
|
-/**
|
|
|
- * 单选:选中 key 的同时把同组其它脚本清掉;on=false 则整组清空
|
|
|
- * (回到设计稿 16:23 的「未选择脚本」状态)。
|
|
|
- *
|
|
|
- * 为什么要在页面里做而不是让组件做:单选是**本页的业务规则**(设计稿的
|
|
|
- * 「同一时间仅可运行一个脚本」),而 UID私信 / 养号 / 视频功能三页是可以多选的。
|
|
|
- * 把规则放进组件,别的页面就被迫一起变成单选。
|
|
|
- */
|
|
|
-Pages.toggleIdScript = function (key, on) {
|
|
|
- for (var i = 0; i < Pages.ID_SCRIPTS.length; i++) {
|
|
|
- var k = Pages.ID_SCRIPTS[i].key;
|
|
|
- Store.set("tabs.id.scripts." + k + ".checked", on ? k === key : false);
|
|
|
+/** 取某个表里 key 的下标(选了之后要把文案换掉,回调给的是下标)。 */
|
|
|
+function indexOfKey(table, key) {
|
|
|
+ for (var i = 0; i < table.length; i++) {
|
|
|
+ if (table[i].key === key) { return i; }
|
|
|
}
|
|
|
-};
|
|
|
-
|
|
|
-/* ------------------------------------------------------------ 脚本卡 */
|
|
|
+ return 0;
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
- * 「开关 + 标签 +(勾上才出现的一个控件)」。
|
|
|
+ * 切换"取哪些列表"里的某一项。
|
|
|
*
|
|
|
- * 四个脚本里反复出现这种形态(主播去重 / 使用文本 ID / AI 头像识别…),
|
|
|
- * 抽出来是因为它有一个**必须一起做对**的细节:右侧控件只在开关打开时才 addView。
|
|
|
- * 各写各的很容易漏掉「关掉后右侧还留着」,那会让用户以为没关掉。
|
|
|
- *
|
|
|
- * @param opts.path 开关绑定的配置路径
|
|
|
- * @param opts.label 标签
|
|
|
- * @param opts.rightOf (on) => View|null,返回开关打开时要显示在右侧的控件
|
|
|
- * @param opts.rerender true → 切换后整页重建(右侧控件的出现/消失需要它)
|
|
|
+ * **最后一个不许取消**:三个都取消,这条取材就没有任何来源可抓 ——
|
|
|
+ * 它会变成一条永远跑不出结果的取材,而用户看不出为什么。
|
|
|
*/
|
|
|
-function buildToggleRow(opts) {
|
|
|
- var on = Store.getBool(opts.path);
|
|
|
- var line = U.row();
|
|
|
+function toggleListOf(itemId, key, next) {
|
|
|
+ var cur = Sources.find(itemId);
|
|
|
+ if (!cur) { return; }
|
|
|
+
|
|
|
+ var list = cur.lists.slice();
|
|
|
+ var at = list.indexOf(key);
|
|
|
+
|
|
|
+ if (next && at < 0) {
|
|
|
+ list.push(key);
|
|
|
+ } else if (!next && at >= 0) {
|
|
|
+ if (list.length === 1) {
|
|
|
+ Dialog.alert("至少要留一个列表",
|
|
|
+ "这条取材要取哪个列表?\n\n三个都取消的话,它就没有任何来源可抓了。");
|
|
|
+ Pages.requestRerender();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ list.splice(at, 1);
|
|
|
+ } else {
|
|
|
+ return; /* 状态没变,不必写盘也不必重建 */
|
|
|
+ }
|
|
|
|
|
|
- var box = W.checkRow(opts.label, on, function (next) {
|
|
|
- Store.set(opts.path, next);
|
|
|
- if (opts.rerender) { Pages.requestRerender(); }
|
|
|
- });
|
|
|
- line.addView(box.root);
|
|
|
+ Sources.update(itemId, { lists: list });
|
|
|
+ Pages.requestRerender();
|
|
|
+}
|
|
|
|
|
|
- var right = on && opts.rightOf ? opts.rightOf(on) : null;
|
|
|
- if (right) {
|
|
|
- line.addView(U.flexible());
|
|
|
- line.addView(U.hgap(8));
|
|
|
- line.addView(right);
|
|
|
+/** 「取哪些列表」多选行。 */
|
|
|
+function buildListPicker(item) {
|
|
|
+ var row = U.row();
|
|
|
+ for (var i = 0; i < Sources.LISTS.length; i++) {
|
|
|
+ (function (entry, at) {
|
|
|
+ var on = item.lists.indexOf(entry.key) >= 0;
|
|
|
+ var box = W.checkRow(entry.label, on, function (next) {
|
|
|
+ toggleListOf(item.id, entry.key, next);
|
|
|
+ });
|
|
|
+ if (at > 0) { row.addView(U.hgap(10)); }
|
|
|
+ row.addView(box.root);
|
|
|
+ })(Sources.LISTS[i], i);
|
|
|
}
|
|
|
- return line;
|
|
|
+ return row;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 脚本一 · 粉丝页提 ID(设计稿 3:244;v7 清语义)。
|
|
|
- *
|
|
|
- * v7 改了三处,都是"让每个控件只有一种含义":
|
|
|
- * · 删「自定关注或粉丝页面提 ID」—— 它与下面的「从哪个页面取」指的是同一件事,
|
|
|
- * 两个控件表达一个意思,用户只能猜"关掉它"和"取消勾选这张卡"有什么区别;
|
|
|
- * · 删「主播去重 + 去重模式」—— 去重收到「功能设置」,全页只出现一次;
|
|
|
- * · 「自定页面提 ID」补上关键词输入 —— 原来勾上之后没说搜什么,无从执行。
|
|
|
- *
|
|
|
- * 顺序改成「主路径 → 额外来源」:先把"从哪个页面取 + 主播账号"放一起,
|
|
|
- * 这两项合起来才构成一次完整取材;搜索用户与文本是两个并列的额外入口。
|
|
|
- */
|
|
|
-function buildIdScript1Detail() {
|
|
|
- var base = "tabs.id.scripts.s1.";
|
|
|
- var rows = [];
|
|
|
-
|
|
|
- /* —— 主路径:主播账号 → 它的哪个列表 —— */
|
|
|
- rows.push(W.settingRow("从哪个页面取",
|
|
|
- W.dropdown("enterPage", base + "enterPage").root, 8));
|
|
|
-
|
|
|
- /* 主播账号:可填多个,用 # 隔开;留空表示不限定、从当前屏幕的列表直接抓 */
|
|
|
- rows.push(W.settingRow("主播账号",
|
|
|
- W.inlineField({
|
|
|
- path: base + "anchorIds",
|
|
|
- placeholder: "账号1#账号2#账号3",
|
|
|
- widthDp: 148
|
|
|
- }).root, 8));
|
|
|
-
|
|
|
- rows.push(W.hint("留空 = 从当前屏幕上的列表直接抓", false));
|
|
|
-
|
|
|
- /* —— 额外来源 1:搜索用户(补上缺的关键词,否则勾了也不知道搜什么) —— */
|
|
|
- var pageBox = W.checkRow("搜索用户提 ID",
|
|
|
- Store.getBool(base + "customPage"),
|
|
|
- function (on) {
|
|
|
- Store.set(base + "customPage", on);
|
|
|
- /* 勾上之后右侧要多出关键词输入框,只能重建这一页 */
|
|
|
- Pages.requestRerender();
|
|
|
- });
|
|
|
- var pageRow = U.row();
|
|
|
- pageRow.addView(pageBox.root);
|
|
|
- if (Store.getBool(base + "customPage")) {
|
|
|
- pageRow.addView(U.flexible());
|
|
|
- pageRow.addView(U.hgap(8));
|
|
|
- pageRow.addView(W.inlineField({
|
|
|
- path: base + "customPageKeyword",
|
|
|
- placeholder: "搜索关键词",
|
|
|
- widthDp: 100,
|
|
|
- align: "right"
|
|
|
- }).root);
|
|
|
- }
|
|
|
- rows.push(pageRow);
|
|
|
-
|
|
|
- /* —— 额外来源 2:本地文本 —— */
|
|
|
- var useBox = W.checkRow("使用文本 ID",
|
|
|
- Store.getBool(base + "useTextId"),
|
|
|
- function (on) {
|
|
|
- Store.set(base + "useTextId", on);
|
|
|
- /* 勾上之后右侧要多出「路径 + 选择文件」,只能重建这一页 */
|
|
|
- Pages.requestRerender();
|
|
|
+/** 起点下拉(选完要重排输入行,所以必须重建整页)。 */
|
|
|
+function buildKindChooser(item) {
|
|
|
+ return W.chooser({
|
|
|
+ label: Sources.kindLabel(item.kind),
|
|
|
+ options: labelsOf(Sources.KINDS),
|
|
|
+ index: function () {
|
|
|
+ var cur = Sources.find(item.id);
|
|
|
+ return indexOfKey(Sources.KINDS, cur ? cur.kind : "anchor");
|
|
|
+ },
|
|
|
+ onPick: function (i) {
|
|
|
+ /* 换起点 → normalize 会把旧起点特有的参数丢掉(见 Sources.normalize 的第 ① 条) */
|
|
|
+ Sources.update(item.id, { kind: Sources.KINDS[i].key });
|
|
|
+ Pages.requestRerender();
|
|
|
+ },
|
|
|
+ minWidthDp: 110
|
|
|
});
|
|
|
+}
|
|
|
|
|
|
- /* 路径本身就可在栏位里直接改(可以手输,不一定非走「选择文件」) */
|
|
|
- var pathField = W.inlineField({
|
|
|
- path: base + "textIdPath",
|
|
|
- placeholder: "文本路径",
|
|
|
- widthDp: 110,
|
|
|
- align: "right"
|
|
|
+/** 深度下拉。`screen` 起点已经是最底层,不给这个选项。 */
|
|
|
+function buildExpandChooser(item) {
|
|
|
+ if (!Sources.canExpand(item.kind)) { return null; }
|
|
|
+ return W.chooser({
|
|
|
+ label: Sources.expandLabel(item.expand),
|
|
|
+ options: labelsOf(Sources.EXPANDS),
|
|
|
+ index: function () {
|
|
|
+ var cur = Sources.find(item.id);
|
|
|
+ return indexOfKey(Sources.EXPANDS, cur ? cur.expand : "");
|
|
|
+ },
|
|
|
+ onPick: function (i) {
|
|
|
+ Sources.update(item.id, { expand: Sources.EXPANDS[i].key });
|
|
|
+ Pages.requestRerender();
|
|
|
+ },
|
|
|
+ minWidthDp: 104
|
|
|
});
|
|
|
+}
|
|
|
|
|
|
- var useRow = U.row();
|
|
|
- useRow.addView(useBox.root);
|
|
|
- if (Store.getBool(base + "useTextId")) {
|
|
|
- useRow.addView(U.flexible());
|
|
|
- useRow.addView(pathField.root);
|
|
|
- useRow.addView(U.hgap(6));
|
|
|
- useRow.addView(W.filePickerButton(base + "textIdPath", pathField.paint).root);
|
|
|
- }
|
|
|
- rows.push(useRow);
|
|
|
-
|
|
|
- rows.push(W.hint("以上两项是额外的取材来源,可与主播账号同时使用", false));
|
|
|
-
|
|
|
- var detail = U.column();
|
|
|
- W.stack(detail, rows, 12);
|
|
|
- return detail;
|
|
|
+/** 一个绑到取材字段的输入框(不是 Store 字段,所以用 read/write 而不是 path)。 */
|
|
|
+function sourceField(item, key, placeholder, widthDp) {
|
|
|
+ return W.inlineField({
|
|
|
+ read: function () {
|
|
|
+ var cur = Sources.find(item.id);
|
|
|
+ return cur ? cur[key] : "";
|
|
|
+ },
|
|
|
+ write: function (value) {
|
|
|
+ var patch = {};
|
|
|
+ patch[key] = value;
|
|
|
+ Sources.update(item.id, patch);
|
|
|
+ },
|
|
|
+ placeholder: placeholder,
|
|
|
+ widthDp: widthDp
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 脚本二 · 评论页提 ID(设计稿 16:483;v7 换掉一个纯开关)。
|
|
|
- *
|
|
|
- * v7:「自定评论页面提 ID」勾上只说"用自定义的评论页面",却没说**是哪条视频的**
|
|
|
- * —— 执行时无从下手。换成一个真正决定取材起点的维度:视频从哪来。
|
|
|
- * ("当前屏幕"= 用户已经自己停在某个评论区,这是最常见也最省事的用法。)
|
|
|
- */
|
|
|
-function buildIdScript2Detail() {
|
|
|
- var base = "tabs.id.scripts.s2.";
|
|
|
+/** 一条取材的输入区 —— **按起点分派**,选了什么才出现什么。 */
|
|
|
+function buildSourceInputs(item) {
|
|
|
var rows = [];
|
|
|
|
|
|
- rows.push(W.settingRow("视频从哪来",
|
|
|
- W.dropdown("videoSource", base + "videoSource", function () {
|
|
|
- /* 选「关键词搜索」才需要关键词输入,得重建这一页 */
|
|
|
- Pages.requestRerender();
|
|
|
- }).root, 8));
|
|
|
-
|
|
|
- if (Store.get("tabs.id.scripts.s2.videoSource") === "keyword") {
|
|
|
- rows.push(W.settingRow("搜索关键词",
|
|
|
- W.inlineField({
|
|
|
- path: base + "videoKeyword",
|
|
|
- placeholder: "关键词",
|
|
|
- widthDp: 148
|
|
|
- }).root, 8));
|
|
|
- } else {
|
|
|
- rows.push(W.hint("当前屏幕 = 用你此刻停在的那条视频的评论区", false));
|
|
|
+ /* —— 指定账号 —— */
|
|
|
+ if (item.kind === "anchor") {
|
|
|
+ rows.push(W.settingRow("账号",
|
|
|
+ sourceField(item, "anchors", "账号1#账号2#账号3", 148).root, 8));
|
|
|
+ rows.push(W.settingRow("取哪些列表", null, 0));
|
|
|
+ rows.push(buildListPicker(item));
|
|
|
+ rows.push(W.hint("账号留空 = 从当前屏幕上的列表直接抓", false));
|
|
|
}
|
|
|
|
|
|
- rows.push(buildToggleRow({
|
|
|
- path: base + "commenterFans",
|
|
|
- label: "再取评论者的粉丝 ID",
|
|
|
- rerender: false
|
|
|
- }));
|
|
|
+ /* —— 搜索结果 —— */
|
|
|
+ if (item.kind === "search") {
|
|
|
+ rows.push(W.settingRow("搜什么", W.chooser({
|
|
|
+ label: item.searchType === "videos" ? "视频作者" : "用户",
|
|
|
+ options: labelsOf(Sources.SEARCH_TYPES),
|
|
|
+ index: function () {
|
|
|
+ var cur = Sources.find(item.id);
|
|
|
+ return indexOfKey(Sources.SEARCH_TYPES, cur ? cur.searchType : "users");
|
|
|
+ },
|
|
|
+ onPick: function (i) {
|
|
|
+ Sources.update(item.id, { searchType: Sources.SEARCH_TYPES[i].key });
|
|
|
+ Pages.requestRerender();
|
|
|
+ },
|
|
|
+ minWidthDp: 96
|
|
|
+ }).root, 8));
|
|
|
+ rows.push(W.settingRow("关键词",
|
|
|
+ sourceField(item, "keyword", "关键词", 148).root, 8));
|
|
|
+ }
|
|
|
|
|
|
- var detail = U.column();
|
|
|
- W.stack(detail, rows, 12);
|
|
|
- return detail;
|
|
|
-}
|
|
|
+ /* —— 某个视频的评论区 —— */
|
|
|
+ if (item.kind === "video") {
|
|
|
+ rows.push(W.settingRow("视频从哪来", W.chooser({
|
|
|
+ label: item.videoSource === "keyword" ? "关键词搜索" : "当前屏幕",
|
|
|
+ options: labelsOf(Sources.VIDEO_SOURCES),
|
|
|
+ index: function () {
|
|
|
+ var cur = Sources.find(item.id);
|
|
|
+ return indexOfKey(Sources.VIDEO_SOURCES, cur ? cur.videoSource : "screen");
|
|
|
+ },
|
|
|
+ onPick: function (i) {
|
|
|
+ Sources.update(item.id, { videoSource: Sources.VIDEO_SOURCES[i].key });
|
|
|
+ Pages.requestRerender();
|
|
|
+ },
|
|
|
+ minWidthDp: 104
|
|
|
+ }).root, 8));
|
|
|
|
|
|
-/**
|
|
|
- * 脚本三 · 文本 UID 裂变提 ID(设计稿 16:483;v7 删两个开关)。
|
|
|
- *
|
|
|
- * v7:
|
|
|
- * · 删「文本 UID 裂变提 ID」—— 与卡片名一字不差,勾不勾都说不通;
|
|
|
- * · 删「UID 博主去重」—— 与脚本一的去重是同一件事,已收到「功能设置」;
|
|
|
- * · 「进入页面」→「展开哪个列表」,并换成只含粉丝/关注两个值的 `expandList`
|
|
|
- * (原来那套 4 个值里有「评论页」「主页」,在"展开一个列表"的语义下根本不存在)。
|
|
|
- */
|
|
|
-function buildIdScript3Detail() {
|
|
|
- var base = "tabs.id.scripts.s3.";
|
|
|
- var rows = [];
|
|
|
+ if (item.videoSource === "keyword") {
|
|
|
+ rows.push(W.settingRow("搜索关键词",
|
|
|
+ sourceField(item, "videoKeyword", "关键词", 148).root, 8));
|
|
|
+ } else {
|
|
|
+ rows.push(W.hint("当前屏幕 = 用你此刻停在的那条视频的评论区", false));
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- rows.push(W.settingRow("展开哪个列表",
|
|
|
- W.dropdown("expandList", base + "enterPage").root, 8));
|
|
|
+ /* —— 文本文件 —— */
|
|
|
+ if (item.kind === "text") {
|
|
|
+ var field = sourceField(item, "path", "文本路径", 108);
|
|
|
+ var pathRow = U.row();
|
|
|
+ pathRow.addView(U.text("文件", Theme.m.labelSize, Theme.hex.textSecondary, false));
|
|
|
+ pathRow.addView(U.flexible());
|
|
|
+ pathRow.addView(field.root);
|
|
|
+ pathRow.addView(U.hgap(6));
|
|
|
+ /* storePath 传 null:只让选择器回调路径,不写 Store(取材字段不在字段表里) */
|
|
|
+ pathRow.addView(W.filePickerButton(null, function (picked) {
|
|
|
+ Sources.update(item.id, { path: picked });
|
|
|
+ field.paint();
|
|
|
+ }).root);
|
|
|
+ rows.push(pathRow);
|
|
|
+ rows.push(W.hint("一行一个 UID;空行与 # 开头的行会被跳过", false));
|
|
|
+ }
|
|
|
|
|
|
- rows.push(W.filePicker("裂变文本", base + "fissionTextPath", "选择").root);
|
|
|
+ /* —— 当前屏幕 —— */
|
|
|
+ if (item.kind === "screen") {
|
|
|
+ rows.push(W.hint("用你此刻停在的那个列表(粉丝页 / 关注页 / 评论区都行)", false));
|
|
|
+ }
|
|
|
|
|
|
- rows.push(W.hint("对文本里的每个 UID,取它上面选的那个列表", false));
|
|
|
+ /* —— 深度(所有起点共用,screen 除外) —— */
|
|
|
+ var expand = buildExpandChooser(item);
|
|
|
+ if (expand) {
|
|
|
+ rows.push(W.settingRow("深度", expand.root, 8));
|
|
|
+ }
|
|
|
|
|
|
- var detail = U.column();
|
|
|
- W.stack(detail, rows, 12);
|
|
|
- return detail;
|
|
|
+ return rows;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 脚本四 · 关键词视频提 ID(设计稿 16:589;v7 删一个开关 + 补层级)。
|
|
|
- *
|
|
|
- * v7:
|
|
|
- * · 删「关键词视频提 ID」(与卡名一字不差);
|
|
|
- * · 三个「提取…」原来是四项平级排列,读不出"第 1 项是根、后 3 项是扩展",
|
|
|
- * 更读不出"评论区与作者粉丝是同一层、评论者的粉丝更深一层" ——
|
|
|
- * 现在加缩进 + 一句说明,让层级在视觉上成立。
|
|
|
- */
|
|
|
-function buildIdScript4Detail() {
|
|
|
- var base = "tabs.id.scripts.s4.";
|
|
|
- var rows = [];
|
|
|
+/** 一条取材卡片。 */
|
|
|
+function buildSourceCard(item, index) {
|
|
|
+ var card = W.card(false);
|
|
|
|
|
|
- rows.push(W.settingRow("关键词",
|
|
|
- W.inlineField({
|
|
|
- path: base + "keyword",
|
|
|
- placeholder: "关键词",
|
|
|
- widthDp: 148
|
|
|
- }).root, 8));
|
|
|
+ var head = U.row();
|
|
|
+ head.addView(U.text("第 " + (index + 1) + " 条", Theme.m.labelSize, Theme.hex.textPrimary, true));
|
|
|
+ head.addView(U.flexible());
|
|
|
+ var remove = W.smallButton("删除", "danger", function () {
|
|
|
+ Sources.removeAt(index);
|
|
|
+ Pages.requestRerender();
|
|
|
+ });
|
|
|
+ head.addView(remove.root);
|
|
|
|
|
|
- rows.push(W.hint("默认只提取视频作者的 ID;下面三项是额外的取材范围", false));
|
|
|
+ var rows = [head];
|
|
|
+ rows.push(U.spacer(4));
|
|
|
+ rows.push(W.settingRow("起点", buildKindChooser(item).root, 8));
|
|
|
|
|
|
- var extras = [
|
|
|
- { path: "anchorFans", label: "作者的粉丝 ID" },
|
|
|
- { path: "commentArea", label: "评论区里的 ID" },
|
|
|
- { path: "commenterFans", label: "评论者的粉丝 ID" }
|
|
|
- ];
|
|
|
- for (var i = 0; i < extras.length; i++) {
|
|
|
- var row = buildToggleRow({
|
|
|
- path: base + extras[i].path,
|
|
|
- label: extras[i].label,
|
|
|
- rerender: false
|
|
|
- });
|
|
|
- /* 缩进表达层级:这三项是"在作者之外再取",与上面的关键词不是平级。
|
|
|
- * 用左内边距而不是加 "└" 之类的符号 —— 符号要占字宽,标签一长就会被挤。 */
|
|
|
- row.setPadding(U.dp(16), U.dp(4), 0, U.dp(4));
|
|
|
- rows.push(row);
|
|
|
- }
|
|
|
+ var inputs = buildSourceInputs(item);
|
|
|
+ for (var i = 0; i < inputs.length; i++) { rows.push(inputs[i]); }
|
|
|
|
|
|
- var detail = U.column();
|
|
|
- W.stack(detail, rows, 12);
|
|
|
- return detail;
|
|
|
+ W.stack(card, rows, 10);
|
|
|
+ return card;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * key → 展开后的设置区。
|
|
|
- *
|
|
|
- * 四个脚本做的事各不相同(粉丝页 / 评论页 / 文本 UID / 关键词视频),
|
|
|
- * 设置项也就各不相同 —— 早先只有脚本一有内容,另外三张展开是空的,
|
|
|
- * 用户点开折叠箭头会以为坏了。
|
|
|
- */
|
|
|
-Pages.ID_DETAILS = {
|
|
|
- s1: buildIdScript1Detail,
|
|
|
- s2: buildIdScript2Detail,
|
|
|
- s3: buildIdScript3Detail,
|
|
|
- s4: buildIdScript4Detail
|
|
|
-};
|
|
|
-
|
|
|
-/** 单张脚本卡。折叠态只出头部一行,展开态追加设置区。 */
|
|
|
-function buildIdScriptCard(script) {
|
|
|
- var base = "tabs.id.scripts." + script.key + ".";
|
|
|
- var checked = Store.getBool(base + "checked");
|
|
|
- var expanded = Store.getBool(base + "expanded");
|
|
|
- var running = Session.running;
|
|
|
-
|
|
|
- var card = W.card(checked);
|
|
|
- card.addView(W.scriptHeader({
|
|
|
- checked: checked,
|
|
|
- expanded: expanded,
|
|
|
- name: script.name,
|
|
|
- mode: "radio",
|
|
|
- /* 运行中不允许换脚本:设计稿 09 的底部提示就是这么写的,
|
|
|
- * 而且真换了脚本,状态条上显示的名字就与正在跑的不是同一个了。 */
|
|
|
- disabled: running,
|
|
|
- /* 展开后摘要位置让给设置行,避免一行里挤三样东西 */
|
|
|
- summary: expanded ? null : script.summary,
|
|
|
- onToggleCheck: function (next) {
|
|
|
- Pages.toggleIdScript(script.key, next);
|
|
|
- Pages.requestRerender();
|
|
|
- },
|
|
|
- onToggleExpand: function () {
|
|
|
- Store.set(base + "expanded", !expanded);
|
|
|
- Pages.requestRerender();
|
|
|
- }
|
|
|
- }).root);
|
|
|
-
|
|
|
- if (expanded) {
|
|
|
- var build = Pages.ID_DETAILS[script.key];
|
|
|
- if (build) {
|
|
|
- card.addView(U.spacer(12));
|
|
|
- card.addView(build());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return card;
|
|
|
+/** 一条取材都没有时的占位 —— 不用 `W.emptyState`(它靠 weight 撑满整屏,在滚动页里不成立)。 */
|
|
|
+function buildSourcesEmpty() {
|
|
|
+ var box = U.column();
|
|
|
+ box.setBackground(U.strokeBg(0.03, 0.10, "#FFFFFF", Theme.m.cardRadius));
|
|
|
+ box.setPadding(U.dp(16), U.dp(22), U.dp(16), U.dp(22));
|
|
|
+
|
|
|
+ var head = U.text("还没有取材", 13, Theme.hex.textSecondary, true);
|
|
|
+ head.setGravity(android.view.Gravity.CENTER);
|
|
|
+ box.addView(head);
|
|
|
+ box.addView(U.spacer(6));
|
|
|
+
|
|
|
+ var sub = U.text("点下面的「添加一条取材」,选从哪儿找人", Theme.m.hintSize,
|
|
|
+ Theme.hex.textFaint, false);
|
|
|
+ sub.setGravity(android.view.Gravity.CENTER);
|
|
|
+ box.addView(sub);
|
|
|
+ return box;
|
|
|
}
|
|
|
|
|
|
-/* ------------------------------------------------------------ 功能设置 */
|
|
|
+/* ============================================== ② 只要这样的账号 */
|
|
|
|
|
|
-/** 卡内分组标题。
|
|
|
- * 比 W.sectionTitle 轻一档:功能设置是**一张卡里的三个块**,不是三个页面段,
|
|
|
- * 用页面级标题会把三组切成三个互不相干的区域。 */
|
|
|
+/** 卡内分组标题(与 v7 一致:比 sectionTitle 轻一档)。 */
|
|
|
function featureGroup(title) {
|
|
|
var view = U.text(title, 11, Theme.hex.textMuted, true);
|
|
|
view.setPadding(0, U.dp(8), 0, 0);
|
|
|
@@ -363,48 +294,31 @@ function featureGroup(title) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 功能设置卡(设计稿 16:733;v7 重排为三组)。
|
|
|
+ * ② 过滤:所有取材共用。
|
|
|
*
|
|
|
- * v7 做了三件事:
|
|
|
- * ① **分三组**:原来 19 个字段平铺,混了"过滤条件 / 检测方式 / 待支持项"三种性质,
|
|
|
- * 用户读不出哪些会真的丢人。现在按「只要这样的账号 / 数值门槛 / 去重与输出」分组。
|
|
|
- * ② **置灰两项**:提取区域、AI 画像(头像·性别·年龄)当前没有承载者 ——
|
|
|
- * 不再给"能开却无效"的控件,改成灰行 + 「暂不支持」标记,点一行看原因。
|
|
|
- * 值仍保留在配置里,将来接上直接生效。
|
|
|
- * ③ **去重收口**:脚本卡里的「主播去重 + 去重模式」「UID 博主去重」全部搬到这里,
|
|
|
- * 用「本地去重 / 跨轮去重」两个说人话的开关表达(全页只出现一次)。
|
|
|
- *
|
|
|
- * 关注量/粉丝量/获赞量/视频量仍用 `W.rangeRow` —— 「最小值 ~ 最大值」一个区间占一行,
|
|
|
- * 但两个数各自一个胶囊、各自输入。与 UID私信页的「关注上限 / 私信上限」不冲突:
|
|
|
- * 那边是两个**各自独立成立**的量,该分开;这里的 min/max 是**同一个量的下界与上界**,
|
|
|
- * 拆两行反而像两个不相干的设置。
|
|
|
+ * 这里**一项都不删**(v7 的成果):私密 / 无头像 / 语言 / 取样范围 / 四组数量门槛。
|
|
|
+ * 只有「提取区域」「AI 画像识别」两项当前没有承载者 → 灰行 + 暂不支持
|
|
|
+ * (不给一个能开却什么也不会发生的开关)。
|
|
|
*/
|
|
|
-function buildIdFeatures() {
|
|
|
+function buildAccountFilters() {
|
|
|
var card = W.card(false);
|
|
|
var base = "tabs.id.features.";
|
|
|
var blocks = [];
|
|
|
|
|
|
- /* ══════════════════ 组 1 · 只要这样的账号 ══════════════════ */
|
|
|
- blocks.push(featureGroup("只要这样的账号"));
|
|
|
+ blocks.push(featureGroup("账号特征"));
|
|
|
|
|
|
- /* 过滤私密 / 无头像:两个勾选框并排(设计稿 16:743) */
|
|
|
var privateRow = W.checkRow("过滤私密账户", Store.getBool(base + "filterPrivate"),
|
|
|
function (on) { Store.set(base + "filterPrivate", on); });
|
|
|
var noAvatarRow = W.checkRow("过滤无头像账户", Store.getBool(base + "filterNoAvatar"),
|
|
|
function (on) { Store.set(base + "filterNoAvatar", on); });
|
|
|
var filterLine = U.row();
|
|
|
- /* weight=1 在这里是安全的:两个控件同源、都只是"勾选框+短标签",
|
|
|
- * 不会出现一边 MATCH_PARENT 把另一边挤成 0 宽的情况。 */
|
|
|
U.params(privateRow.root, 0, -2, 1);
|
|
|
U.params(noAvatarRow.root, 0, -2, 1);
|
|
|
filterLine.addView(privateRow.root);
|
|
|
filterLine.addView(noAvatarRow.root);
|
|
|
blocks.push(filterLine);
|
|
|
|
|
|
- /* 语言区域 + 取样范围。
|
|
|
- * 「取样范围」原来叫「昵称个签检测」,与「语言区域」并排却看不出两者的关系 ——
|
|
|
- * 它其实是语言的**取样字段**(在昵称里查 / 在个签里查 / 都查),
|
|
|
- * 而不是一个独立的过滤条件。改名之后这一行的读法就成立了。 */
|
|
|
+ /* 语言区域 + 取样范围:后者是前者的**取样字段**,不是独立的过滤条件 */
|
|
|
var langRow = U.row();
|
|
|
langRow.addView(U.text("语言区域", Theme.m.labelSize, Theme.hex.textSecondary, false));
|
|
|
langRow.addView(U.hgap(8));
|
|
|
@@ -415,9 +329,6 @@ function buildIdFeatures() {
|
|
|
langRow.addView(W.dropdown("nicknameCheck", base + "nicknameCheck").root);
|
|
|
blocks.push(langRow);
|
|
|
|
|
|
- /* —— 以下两项:执行侧还没有承载者 ——
|
|
|
- * 不给可操作控件。原来「提取区域」是个输入框 + 一个 chip,说明里还写着"勾选后…",
|
|
|
- * 但那一行根本没有勾选框 —— 文案与控件自相矛盾,是最容易被当成 bug 的一处。 */
|
|
|
blocks.push(W.mutedRow("提取区域", Store.getText(base + "regionCodes"), function () {
|
|
|
Dialog.alert("提取区域 · 暂不支持",
|
|
|
"TikTok 的用户页**不显示账号所属国家**,没有可靠的地方能读到它,"
|
|
|
@@ -436,7 +347,6 @@ function buildIdFeatures() {
|
|
|
+ "不如明确告诉你它现在不参与。");
|
|
|
}).root);
|
|
|
|
|
|
- /* ══════════════════ 组 2 · 数值门槛 ══════════════════ */
|
|
|
blocks.push(featureGroup("数值门槛"));
|
|
|
|
|
|
var metrics = [
|
|
|
@@ -450,12 +360,17 @@ function buildIdFeatures() {
|
|
|
blocks.push(W.rangeRow(base + m.key + "Min", base + m.key + "Max", m.label, "~").root);
|
|
|
}
|
|
|
|
|
|
- /* ══════════════════ 组 3 · 去重与输出 ══════════════════ */
|
|
|
- blocks.push(featureGroup("去重与输出"));
|
|
|
+ W.stack(card, blocks, 12);
|
|
|
+ return card;
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================== ③ 输出 */
|
|
|
+
|
|
|
+function buildOutput() {
|
|
|
+ var card = W.card(false);
|
|
|
+ var base = "tabs.id.features.";
|
|
|
+ var blocks = [];
|
|
|
|
|
|
- /* 本地去重 / 跨轮去重。
|
|
|
- * 这两个原来是三处控件(脚本一的「主播去重 + 去重模式」、脚本三的「UID 博主去重」),
|
|
|
- * 分布在两张卡里,用户根本不知道它们是不是同一件事。现在收到一处、说人话。 */
|
|
|
var localBox = W.checkRow("本地去重", Store.getBool(base + "dedupeLocal"),
|
|
|
function (on) { Store.set(base + "dedupeLocal", on); });
|
|
|
var historyBox = W.checkRow("跨轮去重", Store.getBool(base + "dedupeHistory"),
|
|
|
@@ -489,7 +404,6 @@ function buildIdFeatures() {
|
|
|
var testBtn = W.chip("连通测试", "accent");
|
|
|
testBtn.root.setOnClickListener(function () {
|
|
|
var table = Store.getText(base + "cloudTable");
|
|
|
- /* 如实报告:没有服务端地址时就说没配,不假装测试通过 */
|
|
|
Dialog.alert("连通测试",
|
|
|
table === "" ? "请先填写表名。"
|
|
|
: "表名:" + table + "\n\n当前未配置云端去重服务地址,无法真实测试。");
|
|
|
@@ -500,7 +414,7 @@ function buildIdFeatures() {
|
|
|
blocks.push(cloudBox.root);
|
|
|
}
|
|
|
|
|
|
- /* 提取上限。0 的语义写在文案里 —— 只说"0",用户会以为是"提取 0 个" */
|
|
|
+ /* 提取上限:0 的语义写在文案里 —— 只说"0",用户会以为是"提取 0 个" */
|
|
|
var stopRow = U.row();
|
|
|
stopRow.addView(U.text("提取满", Theme.m.labelSize, Theme.hex.textSecondary, false));
|
|
|
stopRow.addView(U.flexible());
|
|
|
@@ -509,58 +423,56 @@ function buildIdFeatures() {
|
|
|
stopRow.addView(U.text("个 ID 后停止(0 = 不限)", Theme.m.labelSize, Theme.hex.textMuted, false));
|
|
|
blocks.push(stopRow);
|
|
|
|
|
|
+ blocks.push(W.hint("结果文件自动命名 uid_日期_时分.txt,存放在 /sdcard/Download/tiktok_app", false));
|
|
|
+
|
|
|
W.stack(card, blocks, 12);
|
|
|
return card;
|
|
|
}
|
|
|
|
|
|
-/* ------------------------------------------------------------ 页面 */
|
|
|
+/* ============================================== 页面 */
|
|
|
|
|
|
-/** 页面主体。rerender 由 main.js 注入,用于交互后重建当前页。 */
|
|
|
Pages.id = function (rerender) {
|
|
|
var inner = U.column();
|
|
|
var blocks = [];
|
|
|
|
|
|
- /* 「当前脚本」提示(设计稿 16:587):只在选了脚本之后出现。
|
|
|
- * 放在最上面是因为它会随滚动被推走,不会长期占地方。 */
|
|
|
- var current = Pages.checkedIdScript();
|
|
|
- if (current) {
|
|
|
- var tip = U.column();
|
|
|
- tip.setBackground(U.solidBg(U.alphaOf("#FFFFFF", Theme.alpha.chipFill), 10));
|
|
|
- tip.setPadding(U.dp(8), U.dp(8), U.dp(8), U.dp(8));
|
|
|
- var tipText = U.text("\u5F53\u524D\u811A\u672C\uFF1A" + Pages.scriptName("id", current),
|
|
|
- 11, Theme.hex.textMuted, false);
|
|
|
- tipText.setGravity(android.view.Gravity.CENTER);
|
|
|
- tip.addView(tipText);
|
|
|
- blocks.push(tip);
|
|
|
- }
|
|
|
-
|
|
|
- blocks.push(W.sectionTitle("\u811A\u672C\u9009\u62E9"));
|
|
|
+ /* ——— ① 取材 ——— */
|
|
|
+ blocks.push(W.sectionTitle("取材"));
|
|
|
+ blocks.push(W.hint("从哪儿找人。可添加多条,按顺序执行;共用同一份去重与输出", false));
|
|
|
|
|
|
- /* 单选说明。放在标题下面而不是塞进卡片:它约束的是「整组」,
|
|
|
- * 写进某一张卡会让人以为只对那张卡生效。 */
|
|
|
- var rule = U.text("\u5355\u9009\uFF1A\u540C\u4E00\u65F6\u95F4\u4EC5\u53EF\u8FD0\u884C\u4E00\u4E2A\u811A\u672C",
|
|
|
- 11, Theme.hex.textFaint, false);
|
|
|
- blocks.push(rule);
|
|
|
+ var list = Sources.list();
|
|
|
+ if (list.length === 0) {
|
|
|
+ blocks.push(buildSourcesEmpty());
|
|
|
+ } else {
|
|
|
+ for (var i = 0; i < list.length; i++) {
|
|
|
+ blocks.push(buildSourceCard(list[i], i));
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- for (var i = 0; i < Pages.ID_SCRIPTS.length; i++) {
|
|
|
- blocks.push(buildIdScriptCard(Pages.ID_SCRIPTS[i]));
|
|
|
+ if (list.length < Sources.MAX) {
|
|
|
+ var add = W.ghostButton("+ 添加一条取材", 44, function () {
|
|
|
+ if (Sources.count() >= Sources.MAX) { return; }
|
|
|
+ Sources.add("anchor");
|
|
|
+ Pages.requestRerender();
|
|
|
+ });
|
|
|
+ blocks.push(add.root);
|
|
|
+ } else {
|
|
|
+ blocks.push(W.hint("已达上限 " + Sources.MAX + " 条 —— 想加新的先删一条", false));
|
|
|
}
|
|
|
|
|
|
- /* ★ 滚动指引放在**脚本四之后、功能设置之前**(2026-10-13 用户指定位置)。
|
|
|
- * 它指向的就是紧接在下面的那一屏内容,贴着被指向的东西才读得懂;
|
|
|
- * 原先放在页尾时它是自相矛盾的:滚到功能设置的人已经不需要它,
|
|
|
- * 而真正需要它的(还没往下滚的人)恰恰也看不到它 —— 它自己也被折在屏幕外。 */
|
|
|
- blocks.push(W.hint("\u2193 \u4E0B\u65B9\u8FD8\u6709\u300C\u529F\u80FD\u8BBE\u7F6E\u300D\uFF0C\u5411\u4E0B\u6EDA\u52A8\u67E5\u770B", true));
|
|
|
+ /* ——— ② 只要这样的账号 ——— */
|
|
|
+ blocks.push(U.spacer(4));
|
|
|
+ blocks.push(W.sectionTitle("只要这样的账号"));
|
|
|
+ blocks.push(buildAccountFilters());
|
|
|
|
|
|
+ /* ——— ③ 输出 ——— */
|
|
|
blocks.push(U.spacer(4));
|
|
|
- blocks.push(W.sectionTitle("\u529F\u80FD\u8BBE\u7F6E"));
|
|
|
- blocks.push(buildIdFeatures());
|
|
|
+ blocks.push(W.sectionTitle("输出"));
|
|
|
+ blocks.push(buildOutput());
|
|
|
|
|
|
- blocks.push(U.spacer(8));
|
|
|
- /* 运行中的约束提示留在页尾:它约束的是**整页**(不只是脚本卡那一组),
|
|
|
- * 而且它只在运行中出现 —— 与上面的滚动指引不会同时抢同一个位置。 */
|
|
|
+ /* 运行中的约束提示留在页尾:它约束的是整页,而它只在运行中出现 */
|
|
|
if (Session.running) {
|
|
|
- blocks.push(W.hint("\u8FD0\u884C\u4E2D\u4E0D\u53EF\u5207\u6362\u811A\u672C \u00B7 \u8BF7\u4FDD\u6301\u65E0\u969C\u788D\u6743\u9650\u5F00\u542F", true));
|
|
|
+ blocks.push(U.spacer(4));
|
|
|
+ blocks.push(W.hint("运行中不可修改取材 · 请保持无障碍权限开启", true));
|
|
|
}
|
|
|
|
|
|
W.stack(inner, blocks, Theme.m.gapPage);
|