dsh-wallpaper_share 26.9.13 → 26.9.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/README.zh-CN.md +6 -6
- package/lib/client.js +884 -235
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -548,23 +548,6 @@ window.__ModuleLoader__.load({
|
|
|
548
548
|
};
|
|
549
549
|
return { ok: true };
|
|
550
550
|
}
|
|
551
|
-
/** 多候选时切换入口。 */
|
|
552
|
-
async function setEntry(id, file, fetchFn = defaultFetch) {
|
|
553
|
-
const res = await fetchFn("/we-sync/launcher/entry", {
|
|
554
|
-
method: "POST",
|
|
555
|
-
headers: { "Content-Type": "application/json" },
|
|
556
|
-
body: JSON.stringify({
|
|
557
|
-
id,
|
|
558
|
-
file
|
|
559
|
-
})
|
|
560
|
-
});
|
|
561
|
-
const body = await res.json().catch(() => ({}));
|
|
562
|
-
if (!res.ok) return {
|
|
563
|
-
ok: false,
|
|
564
|
-
error: body.error ?? `HTTP ${res.status}`
|
|
565
|
-
};
|
|
566
|
-
return { ok: true };
|
|
567
|
-
}
|
|
568
551
|
/** 139 登录态:查询是否已配置(返回掩码账号)与保存。 */
|
|
569
552
|
async function get139Auth(fetchFn = defaultFetch) {
|
|
570
553
|
const body = await (await fetchFn("/we-sync/launcher/139auth", { cache: "no-store" })).json().catch(() => ({}));
|
|
@@ -623,23 +606,6 @@ window.__ModuleLoader__.load({
|
|
|
623
606
|
return false;
|
|
624
607
|
}
|
|
625
608
|
};
|
|
626
|
-
/** 文件大小的人类可读格式(安装确认弹层展示)。 */
|
|
627
|
-
function humanSize(bytes) {
|
|
628
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
629
|
-
const units = [
|
|
630
|
-
"B",
|
|
631
|
-
"KiB",
|
|
632
|
-
"MiB",
|
|
633
|
-
"GiB"
|
|
634
|
-
];
|
|
635
|
-
let v = bytes;
|
|
636
|
-
let i = 0;
|
|
637
|
-
while (v >= 1024 && i < units.length - 1) {
|
|
638
|
-
v /= 1024;
|
|
639
|
-
i++;
|
|
640
|
-
}
|
|
641
|
-
return (i === 0 ? String(v) : v.toFixed(1)) + " " + units[i];
|
|
642
|
-
}
|
|
643
609
|
//#endregion
|
|
644
610
|
//#region src/client/library-model.ts
|
|
645
611
|
const isApplication = (a) => a.type === "application";
|
|
@@ -697,6 +663,586 @@ window.__ModuleLoader__.load({
|
|
|
697
663
|
if (r === "") return slug;
|
|
698
664
|
return r.replace(/\//g, "\\") + "\\" + slug;
|
|
699
665
|
}
|
|
666
|
+
/** 低于此置信度不填入;[0.5, 0.8) 填入但 UI 应提示"可能"(< 0.5)。 */
|
|
667
|
+
const CONFIDENCE_LOW = .5;
|
|
668
|
+
/** 零宽字符 / 双向控制符:肉眼看不见,但会打断锚点匹配。 */
|
|
669
|
+
const ZERO_WIDTH_RE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/g;
|
|
670
|
+
/**
|
|
671
|
+
* Markdown 链接 / 图片:[label](target "title") → label target
|
|
672
|
+
* title 里常常是同一个 URL(复制粘贴产物),一并丢掉,后面按 URL 去重。
|
|
673
|
+
* target 用非空白、非括号序列匹配:云盘链接里出现括号的情况极少。
|
|
674
|
+
*/
|
|
675
|
+
const MD_LINK_RE = /!?\[([^\]\n]*)\]\(\s*([^()\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
676
|
+
/**
|
|
677
|
+
* 输入 normalize(幂等):换行 / 全角冒号 / Tab / 多空格 / Markdown / 零宽字符 / 连续空行。
|
|
678
|
+
* 只做"不影响语义"的整理,不改动中文正文本身。
|
|
679
|
+
*/
|
|
680
|
+
function normalizeInput(rawText) {
|
|
681
|
+
if (typeof rawText !== "string" || rawText === "") return "";
|
|
682
|
+
let s = rawText;
|
|
683
|
+
s = s.replace(ZERO_WIDTH_RE, "");
|
|
684
|
+
s = s.replace(/\r\n?/g, "\n");
|
|
685
|
+
s = s.replace(/\t/g, " ");
|
|
686
|
+
s = s.replace(MD_LINK_RE, (_m, label, target) => `${label} ${target}`);
|
|
687
|
+
s = s.replace(WRAPPED_URL_RE, (_m, a, b) => a ?? b ?? "");
|
|
688
|
+
s = s.replace(/:/g, ":").replace(/=/g, "=");
|
|
689
|
+
s = s.replace(/[^\S\n]{2,}/g, " ");
|
|
690
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
691
|
+
return s.trim();
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* URL 扫描:http(s):// 开头,遇到空白 / 引号 / 反斜杠 / 括号 / 管道 / CJK / 全角字符即停。
|
|
695
|
+
* 因此"链接:https://a.com/x 复制内容…"、"https://a.com/x需要预留10G"、
|
|
696
|
+
* "(https://a.com/x)"、"(https://a.com/x)" 都能正确截断。
|
|
697
|
+
* 代价:URL 自身带括号(维基式 `Foo_(bar)`)会被截短——分享链接里几乎不会出现。
|
|
698
|
+
*/
|
|
699
|
+
const URL_RE = /https?:\/\/[^\s<>"'`\\|^[\]{}()\u3000-\u303F\u4E00-\u9FFF\uFF00-\uFFEF]+/gi;
|
|
700
|
+
/** URL 允许出现的一个字符(不含空白 / 引号 / 反斜杠 / 括号 / CJK / 全角)。 */
|
|
701
|
+
const URL_CHAR = "[^\\s<>\"'`\\\\|^\\[\\]{}()\\u3000-\\u303F\\u4E00-\\u9FFF\\uFF00-\\uFFEF]";
|
|
702
|
+
/**
|
|
703
|
+
* 换行折断的链接(复制论坛 / 微信正文时的常见形态):上一行以 URL 结尾,下一行是它的续接。
|
|
704
|
+
* 只在两种明确信号下拼接,避免把"URL 后面另起一行的普通词"错接进去:
|
|
705
|
+
* a) 下一行以路径续接符 `/ # ? & = %` 开头;或
|
|
706
|
+
* b) 上一行 URL 停在 `/#?&=%_-`(明显断在结构中间)**且**下一行整行都是 URL 安全字符。
|
|
707
|
+
*/
|
|
708
|
+
const WRAPPED_URL_RE = new RegExp(`(https?:\\/\\/${URL_CHAR}+)\\n(?=[/#?&=%])|(https?:\\/\\/${URL_CHAR}*[/#?&=%_-])\\n(?=[A-Za-z0-9._~:/?#\\[\\]@!$&'()*+,;=%-]+(?:\\n|$))`, "g");
|
|
709
|
+
/**
|
|
710
|
+
* 无协议链接:`host.tld/路径`。
|
|
711
|
+
* 分享文本里很常见(微信 / QQ / 网盘客户端复制出来的正文经常只剩裸域名),
|
|
712
|
+
* 只在前面的 http(s) 扫描一个都没命中时才启用:既避免与正常链接重复,也少惹噪声。
|
|
713
|
+
* 必须带 `/` 路径段,所以 `Game.exe`、散文里的 `example.com`、`版本1.2.3` 都不会命中。
|
|
714
|
+
*/
|
|
715
|
+
const BARE_URL_RE = new RegExp(`(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z]{2,24}(?::\\d{2,5})?\\/${URL_CHAR}*`, "gi");
|
|
716
|
+
/** 裸链接前面不能是这些字符(否则说明它粘在更长的 token / 路径 / 邮箱里)。 */
|
|
717
|
+
const BARE_URL_GLUE_RE = /[\w@./-]/;
|
|
718
|
+
/** URL 末尾常见的标点(中文句读 / 句号 / 右括号等):扫描残留时剪掉。 */
|
|
719
|
+
const URL_TAIL_RE = /[.,;:!?'"`、。,;:!?…·)】》」』]+$/;
|
|
720
|
+
/** 已知云盘 / 文件托管域名 → 加分(只用于"多个 URL 时排序",不是白名单)。 */
|
|
721
|
+
const HOST_SCORES = [
|
|
722
|
+
[/(?:^|\.)yun\.139\.com$/i, .35],
|
|
723
|
+
[/(?:^|\.)(?:caiyun\.139|139)\.com$/i, .2],
|
|
724
|
+
[/(?:^|\.)(?:aliyundrive|alipan)\.com$/i, .3],
|
|
725
|
+
[/(?:^|\.)pan\.quark\.cn$/i, .3],
|
|
726
|
+
[/(?:^|\.)cloud\.189\.cn$/i, .3],
|
|
727
|
+
[/(?:^|\.)123pan\.com$/i, .3],
|
|
728
|
+
[/(?:^|\.)lanzou[a-z]?\.com$/i, .3],
|
|
729
|
+
[/(?:^|\.)(?:115|anxia)\.com$/i, .25],
|
|
730
|
+
[/(?:^|\.)(?:weiyun|drive\.qq)\.com$/i, .25],
|
|
731
|
+
[/(?:^|\.)(?:mega\.nz|mediafire\.com|dropbox\.com|1drv\.ms|onedrive\.live\.com|drive\.google\.com)$/i, .25],
|
|
732
|
+
[/(?:^|\.)(?:github\.com|githubusercontent\.com|gitee\.com|sourceforge\.net|itch\.io|nexusmods\.com|moddb\.com)$/i, .2]
|
|
733
|
+
];
|
|
734
|
+
/** 分享 / 下载特征路径。 */
|
|
735
|
+
const PATH_HINT_RE = /(?:\/share|\/download|\/file|\/s\/|\/d\/|\/f\/|\/p\/|\/w\/i\/|shareweb)/i;
|
|
736
|
+
/** 压缩包 / 安装包扩展名。 */
|
|
737
|
+
const ARCHIVE_EXT_RE = /\.(?:zip|7z|rar|tar|gz|xz|bz2|exe|msi|iso|001)(?:\.\d{3})?$/i;
|
|
738
|
+
/** 去重键:忽略大小写与末尾斜杠,避免同一链接的两种写法被当成两个候选。 */
|
|
739
|
+
function urlDedupeKey(url) {
|
|
740
|
+
return url.toLowerCase().replace(/\/+$/, "");
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 扫描全部 URL 出现位置(含清洗后的起止下标,供"是否落在 URL 内"判断)。
|
|
744
|
+
* 先找带协议的;一个都没有时才退到"裸域名 + 路径"(分享正文里很常见)。
|
|
745
|
+
*/
|
|
746
|
+
function findUrlSpans(text) {
|
|
747
|
+
const spans = [];
|
|
748
|
+
const re = new RegExp(URL_RE.source, "gi");
|
|
749
|
+
for (const m of text.matchAll(re)) {
|
|
750
|
+
const raw = m[0];
|
|
751
|
+
const start = m.index ?? 0;
|
|
752
|
+
const trimmed = raw.replace(URL_TAIL_RE, "");
|
|
753
|
+
if (trimmed === "") continue;
|
|
754
|
+
spans.push({
|
|
755
|
+
url: trimmed,
|
|
756
|
+
start,
|
|
757
|
+
end: start + trimmed.length
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (spans.length > 0) return spans;
|
|
761
|
+
const bare = new RegExp(BARE_URL_RE.source, "gi");
|
|
762
|
+
for (const m of text.matchAll(bare)) {
|
|
763
|
+
const raw = m[0];
|
|
764
|
+
const start = m.index ?? 0;
|
|
765
|
+
if (start > 0 && BARE_URL_GLUE_RE.test(text[start - 1])) continue;
|
|
766
|
+
const trimmed = raw.replace(URL_TAIL_RE, "");
|
|
767
|
+
if (trimmed === "") continue;
|
|
768
|
+
spans.push({
|
|
769
|
+
url: "https://" + trimmed,
|
|
770
|
+
start,
|
|
771
|
+
end: start + trimmed.length
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return spans;
|
|
775
|
+
}
|
|
776
|
+
/** 从任意文本中提取 URL(保持出现顺序、按规范化键去重)。 */
|
|
777
|
+
function extractUrls(text) {
|
|
778
|
+
if (typeof text !== "string" || text === "") return [];
|
|
779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
780
|
+
const out = [];
|
|
781
|
+
for (const span of findUrlSpans(text)) {
|
|
782
|
+
const key = urlDedupeKey(span.url);
|
|
783
|
+
if (seen.has(key)) continue;
|
|
784
|
+
seen.add(key);
|
|
785
|
+
out.push(span.url);
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
/** 单条 URL 的"像不像下载源"打分(0~1),只影响多个候选时的排序。 */
|
|
790
|
+
function scoreSourceUrl(url) {
|
|
791
|
+
let score = .6;
|
|
792
|
+
let host = "";
|
|
793
|
+
let path = "";
|
|
794
|
+
try {
|
|
795
|
+
const u = new URL(url);
|
|
796
|
+
host = u.hostname;
|
|
797
|
+
path = u.pathname;
|
|
798
|
+
} catch {
|
|
799
|
+
return .5;
|
|
800
|
+
}
|
|
801
|
+
for (const [re, add] of HOST_SCORES) if (re.test(host)) {
|
|
802
|
+
score += add;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
if (PATH_HINT_RE.test(path)) score += .15;
|
|
806
|
+
if (ARCHIVE_EXT_RE.test(path)) score += .25;
|
|
807
|
+
return Math.max(0, Math.min(1, score));
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 多 URL 时挑一个最像"下载 / 分享源"的。
|
|
811
|
+
* 只有一个 URL 时置信度 0.9~0.99;多个时按与次优的分差给 0.5 / 0.7 / 0.9——
|
|
812
|
+
* 打平时仍返回第一个(宁可填上让用户改,也不要什么都不填),但置信度只有 0.5。
|
|
813
|
+
*/
|
|
814
|
+
function pickSourceUrl(urls) {
|
|
815
|
+
if (!Array.isArray(urls) || urls.length === 0) return null;
|
|
816
|
+
const scored = urls.map((url) => ({
|
|
817
|
+
url,
|
|
818
|
+
score: scoreSourceUrl(url)
|
|
819
|
+
}));
|
|
820
|
+
scored.sort((a, b) => b.score - a.score);
|
|
821
|
+
const best = scored[0];
|
|
822
|
+
if (scored.length === 1) return {
|
|
823
|
+
...best,
|
|
824
|
+
confidence: Math.min(.99, Math.max(.9, best.score))
|
|
825
|
+
};
|
|
826
|
+
const gap = best.score - scored[1].score;
|
|
827
|
+
const confidence = gap >= .25 ? .9 : gap >= .1 ? .7 : .5;
|
|
828
|
+
return {
|
|
829
|
+
...best,
|
|
830
|
+
confidence
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
/** 是否"云盘分享页"(用于判断未标注用途的密码更可能是提取码还是解压密码)。 */
|
|
834
|
+
const CLOUD_HOST_RE = /(?:^|\.)(?:yun\.139\.com|caiyun\.139\.com|aliyundrive\.com|alipan\.com|pan\.quark\.cn|cloud\.189\.cn|123pan\.com|lanzou[a-z]?\.com|115\.com|weiyun\.com|drive\.qq\.com|mega\.nz|mediafire\.com|dropbox\.com|1drv\.ms|drive\.google\.com)$/i;
|
|
835
|
+
const CLOUD_SHARE_PATH_RE = /(?:#\/|\/s\/|\/share|\/w\/i\/|\/f\/|\/d\/|shareweb)/i;
|
|
836
|
+
function isShareLikeUrl(url) {
|
|
837
|
+
if (typeof url !== "string" || url === "") return false;
|
|
838
|
+
try {
|
|
839
|
+
const u = new URL(url);
|
|
840
|
+
return CLOUD_HOST_RE.test(u.hostname) && CLOUD_SHARE_PATH_RE.test(u.pathname + u.hash);
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* 锚点表。注意:
|
|
847
|
+
* - 中文锚点必须"长者在先",组合成正则时按长度倒序,避免「解压密码」被「密码」先吃掉;
|
|
848
|
+
* - 普通「密码」置信度 0.5,明显低于「解压密码 / 压缩包密码」(0.95)——
|
|
849
|
+
* 它只作为低置信度候选,由 classifyCredential 结合链接类型再猜。
|
|
850
|
+
*/
|
|
851
|
+
const ANCHORS = [
|
|
852
|
+
{
|
|
853
|
+
text: "压缩包解压密码",
|
|
854
|
+
type: "archive",
|
|
855
|
+
confidence: .95,
|
|
856
|
+
labeled: true,
|
|
857
|
+
ascii: false
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
text: "压缩包解压码",
|
|
861
|
+
type: "archive",
|
|
862
|
+
confidence: .95,
|
|
863
|
+
labeled: true,
|
|
864
|
+
ascii: false
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
text: "解压缩密码",
|
|
868
|
+
type: "archive",
|
|
869
|
+
confidence: .95,
|
|
870
|
+
labeled: true,
|
|
871
|
+
ascii: false
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
text: "压缩包密码",
|
|
875
|
+
type: "archive",
|
|
876
|
+
confidence: .95,
|
|
877
|
+
labeled: true,
|
|
878
|
+
ascii: false
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
text: "解压密码",
|
|
882
|
+
type: "archive",
|
|
883
|
+
confidence: .95,
|
|
884
|
+
labeled: true,
|
|
885
|
+
ascii: false
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
text: "解压口令",
|
|
889
|
+
type: "archive",
|
|
890
|
+
confidence: .9,
|
|
891
|
+
labeled: true,
|
|
892
|
+
ascii: false
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
text: "压缩密码",
|
|
896
|
+
type: "archive",
|
|
897
|
+
confidence: .9,
|
|
898
|
+
labeled: true,
|
|
899
|
+
ascii: false
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
text: "解包密码",
|
|
903
|
+
type: "archive",
|
|
904
|
+
confidence: .85,
|
|
905
|
+
labeled: true,
|
|
906
|
+
ascii: false
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
text: "解密密码",
|
|
910
|
+
type: "archive",
|
|
911
|
+
confidence: .85,
|
|
912
|
+
labeled: true,
|
|
913
|
+
ascii: false
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
text: "解压码",
|
|
917
|
+
type: "archive",
|
|
918
|
+
confidence: .95,
|
|
919
|
+
labeled: true,
|
|
920
|
+
ascii: false
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
text: "archive password",
|
|
924
|
+
type: "archive",
|
|
925
|
+
confidence: .9,
|
|
926
|
+
labeled: true,
|
|
927
|
+
ascii: true
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
text: "unzip password",
|
|
931
|
+
type: "archive",
|
|
932
|
+
confidence: .9,
|
|
933
|
+
labeled: true,
|
|
934
|
+
ascii: true
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
text: "zip password",
|
|
938
|
+
type: "archive",
|
|
939
|
+
confidence: .9,
|
|
940
|
+
labeled: true,
|
|
941
|
+
ascii: true
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
text: "rar password",
|
|
945
|
+
type: "archive",
|
|
946
|
+
confidence: .9,
|
|
947
|
+
labeled: true,
|
|
948
|
+
ascii: true
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
text: "extract password",
|
|
952
|
+
type: "archive",
|
|
953
|
+
confidence: .85,
|
|
954
|
+
labeled: true,
|
|
955
|
+
ascii: true
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
text: "分享提取码",
|
|
959
|
+
type: "share",
|
|
960
|
+
confidence: .95,
|
|
961
|
+
labeled: true,
|
|
962
|
+
ascii: false
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
text: "提取码",
|
|
966
|
+
type: "share",
|
|
967
|
+
confidence: .95,
|
|
968
|
+
labeled: true,
|
|
969
|
+
ascii: false
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
text: "提取密码",
|
|
973
|
+
type: "share",
|
|
974
|
+
confidence: .9,
|
|
975
|
+
labeled: true,
|
|
976
|
+
ascii: false
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
text: "访问码",
|
|
980
|
+
type: "share",
|
|
981
|
+
confidence: .9,
|
|
982
|
+
labeled: true,
|
|
983
|
+
ascii: false
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
text: "访问密码",
|
|
987
|
+
type: "share",
|
|
988
|
+
confidence: .9,
|
|
989
|
+
labeled: true,
|
|
990
|
+
ascii: false
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
text: "分享码",
|
|
994
|
+
type: "share",
|
|
995
|
+
confidence: .9,
|
|
996
|
+
labeled: true,
|
|
997
|
+
ascii: false
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
text: "分享密码",
|
|
1001
|
+
type: "share",
|
|
1002
|
+
confidence: .9,
|
|
1003
|
+
labeled: true,
|
|
1004
|
+
ascii: false
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
text: "取件码",
|
|
1008
|
+
type: "share",
|
|
1009
|
+
confidence: .9,
|
|
1010
|
+
labeled: true,
|
|
1011
|
+
ascii: false
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
text: "share code",
|
|
1015
|
+
type: "share",
|
|
1016
|
+
confidence: .9,
|
|
1017
|
+
labeled: true,
|
|
1018
|
+
ascii: true
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
text: "access code",
|
|
1022
|
+
type: "share",
|
|
1023
|
+
confidence: .9,
|
|
1024
|
+
labeled: true,
|
|
1025
|
+
ascii: true
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
text: "share password",
|
|
1029
|
+
type: "share",
|
|
1030
|
+
confidence: .85,
|
|
1031
|
+
labeled: true,
|
|
1032
|
+
ascii: true
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
text: "password",
|
|
1036
|
+
type: "unknown",
|
|
1037
|
+
confidence: .5,
|
|
1038
|
+
labeled: false,
|
|
1039
|
+
ascii: true
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
text: "密码",
|
|
1043
|
+
type: "unknown",
|
|
1044
|
+
confidence: .5,
|
|
1045
|
+
labeled: false,
|
|
1046
|
+
ascii: false
|
|
1047
|
+
}
|
|
1048
|
+
];
|
|
1049
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1050
|
+
/** 锚点原文(小写)→ 定义;正则匹配结果按此回查,避免在正则里塞重复分支。 */
|
|
1051
|
+
const ANCHOR_INDEX = new Map(ANCHORS.map((a) => [a.text.toLowerCase(), a]));
|
|
1052
|
+
/**
|
|
1053
|
+
* 锚点后允许的分隔符(局部窗口只有几个字符):
|
|
1054
|
+
* 空白 / 冒号 / 等号 / "是|为|等于" 的任意组合,最多跨**一个**换行
|
|
1055
|
+
* ("解压密码:\nWS" 在分享帖里很常见)。
|
|
1056
|
+
* 关键性质:这个集合里没有中文字、也没有普通字母,所以它只能前进几个字符,
|
|
1057
|
+
* 不可能把锚点的影响范围拉远——"局部窗口"由此保证。
|
|
1058
|
+
* 这里同时接受全角冒号,便于函数在未 normalize 的文本上直接调用(幂等、不自作聪明)。
|
|
1059
|
+
*/
|
|
1060
|
+
const SEP = "(?:[^\\S\\n]|[:=:=]|是|为|等于)*(?:\\n(?:[^\\S\\n]|[:=:=])*)?[:=:=]?[^\\S\\n]*";
|
|
1061
|
+
/** 密码 token:字母数字 + 常见密码/URL-safe 特殊字符,长度 <= 64(防"把整句吃进来")。 */
|
|
1062
|
+
const TOKEN = "[A-Za-z0-9_\\-@#!.$%^&*+~]{1,64}";
|
|
1063
|
+
const ANCHOR_ALT = [...ANCHORS].sort((a, b) => b.text.length - a.text.length).map((a) => escapeRe(a.text)).join("|");
|
|
1064
|
+
const CRED_RE = new RegExp(`(${ANCHOR_ALT})${SEP}(${TOKEN})`, "gi");
|
|
1065
|
+
/** token 尾部的中文句读 / 英文句号:属于句子而不属于密码。 */
|
|
1066
|
+
const TOKEN_TAIL_RE = /[.,;:!?]+$/;
|
|
1067
|
+
/** 尺寸 / 版本号这类"看着像密码其实不是"的 decoy(仅在弱锚点下过滤)。 */
|
|
1068
|
+
const SIZE_TOKEN_RE = /^\d{1,4}(?:\.\d+)?\s?(?:k|kb|m|mb|g|gb|t|tb|p|pb)$/i;
|
|
1069
|
+
const VERSION_TOKEN_RE = /^\d+(?:\.\d+){2,}$/;
|
|
1070
|
+
/** 前一字符为这些时说明锚点"粘"在 URL / 查询串里。 */
|
|
1071
|
+
const URL_GLUE_RE = /[&=?#]/;
|
|
1072
|
+
/** 前一字符为这些时说明英文锚点粘在更长的英文单词里(mypassword)。 */
|
|
1073
|
+
const WORD_GLUE_RE = /[A-Za-z0-9._-]/;
|
|
1074
|
+
function isInsideSpan(index, spans) {
|
|
1075
|
+
return spans.some((s) => index >= s.start && index < s.end);
|
|
1076
|
+
}
|
|
1077
|
+
function isDecoyToken(value) {
|
|
1078
|
+
return SIZE_TOKEN_RE.test(value) || VERSION_TOKEN_RE.test(value);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* 从整段文本里提取全部密码候选(此时 type 可能仍是 unknown)。
|
|
1082
|
+
* 每一步都受"锚点 + 局部窗口"约束:anchor 后立即取短 token,取不到就放弃——
|
|
1083
|
+
* 绝不从远处捞一个 token 回来(否则「游戏密码机制做得不好」会误伤)。
|
|
1084
|
+
*/
|
|
1085
|
+
function extractCredentialCandidates(text) {
|
|
1086
|
+
if (typeof text !== "string" || text === "") return [];
|
|
1087
|
+
const spans = findUrlSpans(text);
|
|
1088
|
+
const out = [];
|
|
1089
|
+
const re = new RegExp(CRED_RE.source, "gi");
|
|
1090
|
+
for (const m of text.matchAll(re)) {
|
|
1091
|
+
const anchorDef = ANCHOR_INDEX.get((m[1] ?? "").toLowerCase());
|
|
1092
|
+
if (anchorDef === void 0) continue;
|
|
1093
|
+
const index = m.index ?? 0;
|
|
1094
|
+
const value = (m[2] ?? "").replace(TOKEN_TAIL_RE, "");
|
|
1095
|
+
if (value === "" || !/[A-Za-z0-9]/.test(value)) continue;
|
|
1096
|
+
if (isInsideSpan(index, spans)) continue;
|
|
1097
|
+
const prev = text[index - 1];
|
|
1098
|
+
if (prev !== void 0 && URL_GLUE_RE.test(prev)) continue;
|
|
1099
|
+
if (anchorDef.ascii && prev !== void 0 && WORD_GLUE_RE.test(prev)) continue;
|
|
1100
|
+
if (text.slice(index + (m[0]?.length ?? 0), index + (m[0]?.length ?? 0) + 3) === "://") continue;
|
|
1101
|
+
if (!anchorDef.labeled && isDecoyToken(value)) continue;
|
|
1102
|
+
out.push({
|
|
1103
|
+
value,
|
|
1104
|
+
type: anchorDef.type,
|
|
1105
|
+
confidence: anchorDef.confidence,
|
|
1106
|
+
anchor: anchorDef.text,
|
|
1107
|
+
labeled: anchorDef.labeled
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return out;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* 给"用途未知"的候选补一个最可能的分类(不改动已明确的候选):
|
|
1114
|
+
* - 链接像云盘分享页 → 更可能是分享提取码(0.6,低置信度);
|
|
1115
|
+
* - 否则按解压密码处理(0.5,低置信度,UI 应提示"未标注用途,请核对")。
|
|
1116
|
+
* 明确不猜成高置信度:错填密码会让安装直接失败,不如让用户看一眼。
|
|
1117
|
+
*/
|
|
1118
|
+
function classifyCredential(candidate, ctx = {}) {
|
|
1119
|
+
if (candidate.type !== "unknown") return candidate;
|
|
1120
|
+
if (ctx.url !== void 0 && isShareLikeUrl(ctx.url)) return {
|
|
1121
|
+
...candidate,
|
|
1122
|
+
type: "share",
|
|
1123
|
+
confidence: .6
|
|
1124
|
+
};
|
|
1125
|
+
return {
|
|
1126
|
+
...candidate,
|
|
1127
|
+
type: "archive",
|
|
1128
|
+
confidence: .5
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
/** 取某类型里置信度最高的候选(并列取先出现的)。 */
|
|
1132
|
+
function bestCandidate(candidates, type) {
|
|
1133
|
+
let best;
|
|
1134
|
+
for (const c of candidates) {
|
|
1135
|
+
if (c.type !== type) continue;
|
|
1136
|
+
if (best === void 0 || c.confidence > best.confidence) best = c;
|
|
1137
|
+
}
|
|
1138
|
+
return best;
|
|
1139
|
+
}
|
|
1140
|
+
/** 可执行文件:ASCII 文件名字符 + 常见 Windows 可执行后缀(不含 .com,避免把域名当文件)。 */
|
|
1141
|
+
const EXE_RE = /[A-Za-z0-9_][A-Za-z0-9_\-.+]*\.(?:exe|bat|cmd|msi|lnk)\b/gi;
|
|
1142
|
+
/** 锚点关键词(前后局部窗口内出现才算"被指认为启动文件")。 */
|
|
1143
|
+
const LAUNCH_KEYWORD_RE = /(?:启动|运行|打开|双击|主程序|程序入口|入口|可执行|exe\s*文件)|(?:\b(?:launcher|launch|run|open|start|execute)\b)/i;
|
|
1144
|
+
/** 依赖 / 安装器:同名 token 往往是"先装这个再运行主程序",不是入口。 */
|
|
1145
|
+
const DEP_NAME_RE = /^(?:vcredist|vc_redist|dxsetup|directx|dotnet|setup|install|unins|uninstall|update|patch)/i;
|
|
1146
|
+
/** 关键词窗口:锚点在前 40 字符 / 在后 20 字符内都算命中。 */
|
|
1147
|
+
const KEYWORD_BEFORE = 40;
|
|
1148
|
+
const KEYWORD_AFTER = 20;
|
|
1149
|
+
/**
|
|
1150
|
+
* 提取启动文件候选。
|
|
1151
|
+
* .exe + 附近有"运行/启动"类关键词 → 0.95+(自动填入);
|
|
1152
|
+
* 只有 .exe 但无关键词 → 0.75(填入但提示);
|
|
1153
|
+
* 依赖型名字(vcredist / setup…)扣分,避免抢在真正的入口前;
|
|
1154
|
+
* 什么都没有 → null(调用方保持字段为空,不报错)。
|
|
1155
|
+
*/
|
|
1156
|
+
function extractExecutableCandidate(text) {
|
|
1157
|
+
if (typeof text !== "string" || text === "") return null;
|
|
1158
|
+
const spans = findUrlSpans(text);
|
|
1159
|
+
const lower = text.toLowerCase();
|
|
1160
|
+
const found = [];
|
|
1161
|
+
const re = new RegExp(EXE_RE.source, "gi");
|
|
1162
|
+
for (const m of text.matchAll(re)) {
|
|
1163
|
+
const name = m[0];
|
|
1164
|
+
const index = m.index ?? 0;
|
|
1165
|
+
if (isInsideSpan(index, spans)) continue;
|
|
1166
|
+
const before = text.slice(Math.max(0, index - KEYWORD_BEFORE), index);
|
|
1167
|
+
const after = text.slice(index + name.length, index + name.length + KEYWORD_AFTER);
|
|
1168
|
+
const keywordNear = LAUNCH_KEYWORD_RE.test(before) || LAUNCH_KEYWORD_RE.test(after);
|
|
1169
|
+
const count = lower.split(name.toLowerCase()).length - 1;
|
|
1170
|
+
let confidence = .55;
|
|
1171
|
+
if (/\.exe$/i.test(name)) confidence += .2;
|
|
1172
|
+
if (keywordNear) confidence += .25;
|
|
1173
|
+
if (count >= 2) confidence += .05;
|
|
1174
|
+
if (DEP_NAME_RE.test(name)) confidence -= .35;
|
|
1175
|
+
found.push({
|
|
1176
|
+
name,
|
|
1177
|
+
index,
|
|
1178
|
+
count,
|
|
1179
|
+
confidence: Math.max(0, Math.min(1, confidence))
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
if (found.length === 0) return null;
|
|
1183
|
+
found.sort((a, b) => b.confidence - a.confidence || b.count - a.count || a.index - b.index);
|
|
1184
|
+
const best = found[0];
|
|
1185
|
+
return {
|
|
1186
|
+
name: best.name,
|
|
1187
|
+
confidence: best.confidence
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
1191
|
+
/**
|
|
1192
|
+
* 解析一整段分享文本 → 四项安装信息 + 置信度。
|
|
1193
|
+
* 任何字段识别不到就留空(undefined),绝不猜一个高置信度的错值。
|
|
1194
|
+
*/
|
|
1195
|
+
function parseInstallShareText(rawText) {
|
|
1196
|
+
const raw = typeof rawText === "string" ? rawText : "";
|
|
1197
|
+
const confidence = {
|
|
1198
|
+
sourceUrl: 0,
|
|
1199
|
+
shareCode: 0,
|
|
1200
|
+
archivePassword: 0,
|
|
1201
|
+
executableName: 0
|
|
1202
|
+
};
|
|
1203
|
+
const result = {
|
|
1204
|
+
confidence,
|
|
1205
|
+
candidates: [],
|
|
1206
|
+
matched: 0,
|
|
1207
|
+
rawText: raw
|
|
1208
|
+
};
|
|
1209
|
+
let text = "";
|
|
1210
|
+
try {
|
|
1211
|
+
text = normalizeInput(raw);
|
|
1212
|
+
} catch {
|
|
1213
|
+
return result;
|
|
1214
|
+
}
|
|
1215
|
+
if (text === "") return result;
|
|
1216
|
+
const pick = pickSourceUrl(extractUrls(text));
|
|
1217
|
+
const candidates = extractCredentialCandidates(text).map((c) => classifyCredential(c, pick === null ? {} : { url: pick.url }));
|
|
1218
|
+
const share = bestCandidate(candidates, "share");
|
|
1219
|
+
const archive = bestCandidate(candidates, "archive");
|
|
1220
|
+
const exec = extractExecutableCandidate(text);
|
|
1221
|
+
if (pick !== null) {
|
|
1222
|
+
result.sourceUrl = pick.url;
|
|
1223
|
+
confidence.sourceUrl = round2(pick.confidence);
|
|
1224
|
+
}
|
|
1225
|
+
if (share !== void 0) {
|
|
1226
|
+
result.shareCode = share.value;
|
|
1227
|
+
confidence.shareCode = share.confidence;
|
|
1228
|
+
}
|
|
1229
|
+
if (archive !== void 0) {
|
|
1230
|
+
result.archivePassword = archive.value;
|
|
1231
|
+
confidence.archivePassword = archive.confidence;
|
|
1232
|
+
}
|
|
1233
|
+
if (exec !== null) {
|
|
1234
|
+
result.executableName = exec.name;
|
|
1235
|
+
confidence.executableName = round2(exec.confidence);
|
|
1236
|
+
}
|
|
1237
|
+
result.candidates = candidates;
|
|
1238
|
+
result.matched = [
|
|
1239
|
+
confidence.sourceUrl,
|
|
1240
|
+
confidence.shareCode,
|
|
1241
|
+
confidence.archivePassword,
|
|
1242
|
+
confidence.executableName
|
|
1243
|
+
].filter((c) => c >= CONFIDENCE_LOW).length;
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
700
1246
|
//#endregion
|
|
701
1247
|
//#region src/client/WallpaperSharePanel.tsx
|
|
702
1248
|
/**
|
|
@@ -783,11 +1329,9 @@ window.__ModuleLoader__.load({
|
|
|
783
1329
|
manage: "管理",
|
|
784
1330
|
manageDone: "完成",
|
|
785
1331
|
openSource: "打开源文件",
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
detailAddr: "地址",
|
|
790
|
-
detailExe: "exe 文件",
|
|
1332
|
+
launcherColumnName: "名称",
|
|
1333
|
+
launcherColumnLocation: "位置",
|
|
1334
|
+
launcherColumnDownloadedAt: "下载时间",
|
|
791
1335
|
launcherManageHint: "启动与卸载到「本地 → 应用」里操作。",
|
|
792
1336
|
confirmUninstallTitle: "确认卸载?",
|
|
793
1337
|
confirmUninstallBody: "将删除以下内容的本地文件(dwp 壁纸可在市场重装):",
|
|
@@ -859,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
859
1403
|
launcherTutorialPrep3Lead: "点击“",
|
|
860
1404
|
launcherTutorialPrep3Tail: "”(引号内文本为链接)",
|
|
861
1405
|
launcherTutorialUseTitle: "日常使用的流程:",
|
|
862
|
-
launcherTutorialUse1: "1.
|
|
1406
|
+
launcherTutorialUse1: "1. 把分享内容整段粘贴到上方输入框(自动解析),或直接在对话框内填入下载链接、解压密码、提取码、名称",
|
|
863
1407
|
launcherTutorialUse2: "2. 点击「下载安装」",
|
|
864
1408
|
launcherTutorialUse3: "3. 看到应用栏出现应用后,单击启动应用(每次启动有确认弹窗)",
|
|
865
1409
|
launcherAuthPlaceholder: "Basic xxxx… 或 basic:手机号:token",
|
|
@@ -884,6 +1428,21 @@ window.__ModuleLoader__.load({
|
|
|
884
1428
|
launcherShareFail: "分享解析失败(详情见括号内服务端信息)",
|
|
885
1429
|
launcherShareCode139: "该 139 分享需要提取码:请在提取码框填入后重试",
|
|
886
1430
|
launcherShareCode139Wrong: "139 提取码错误,请核对后重试",
|
|
1431
|
+
launcherSmartPasteLabel: "粘贴链接或完整分享内容",
|
|
1432
|
+
launcherSmartPastePlaceholder: "可直接粘贴完整分享文本。系统会自动识别链接、提取码、解压密码和启动文件。",
|
|
1433
|
+
launcherSmartPasteParse: "解析",
|
|
1434
|
+
launcherSmartPasteReparse: "重新解析",
|
|
1435
|
+
launcherSmartPasteClear: "清空",
|
|
1436
|
+
launcherSmartPasteDone: "已识别 {n} 项信息:{items}(可继续手动修改)",
|
|
1437
|
+
launcherSmartPasteLow: ";其中 {items} 为低置信度,请核对",
|
|
1438
|
+
launcherSmartPasteNone: "未从粘贴内容中识别到安装信息,请手动填写下方字段",
|
|
1439
|
+
launcherSmartPasteKept: "识别到信息,但你已手动填写过,未覆盖(点「解析」按钮可覆盖)",
|
|
1440
|
+
launcherSmartPasteKeptDetail: "已识别 {items},但你已手动填写过,未覆盖(点「解析」按钮可覆盖)",
|
|
1441
|
+
launcherSmartPasteSkipNote: ";{items} 保留你的手填内容(点「解析」按钮可覆盖)",
|
|
1442
|
+
launcherFieldUrl: "链接",
|
|
1443
|
+
launcherFieldCode: "提取码",
|
|
1444
|
+
launcherFieldPwd: "解压密码",
|
|
1445
|
+
launcherFieldExe: "启动文件",
|
|
887
1446
|
launcherInstall: "下载安装",
|
|
888
1447
|
launcherInstalling: "下载安装中…",
|
|
889
1448
|
launcherEmpty: "还没有安装的应用。粘贴直链后点「下载安装」。",
|
|
@@ -892,21 +1451,15 @@ window.__ModuleLoader__.load({
|
|
|
892
1451
|
launcherLaunching: "启动中…",
|
|
893
1452
|
launcherOpenFolder: "打开文件夹",
|
|
894
1453
|
launcherUninstall: "卸载",
|
|
895
|
-
launcherSource: "来源",
|
|
896
|
-
launcherSha: "SHA512",
|
|
897
|
-
launcherEntry: "入口",
|
|
898
1454
|
launcherConfirmTitle: "确认启动该程序?",
|
|
899
1455
|
launcherConfirmBody: "将从以下路径执行可执行文件。请确认来源可信:",
|
|
900
1456
|
launcherConfirmGo: "启动",
|
|
901
1457
|
launcherConfirmCancel: "取消",
|
|
902
|
-
launcherSetEntry: "设为入口",
|
|
903
|
-
launcherCandidates: "检测到多个可执行文件,当前入口:",
|
|
904
1458
|
flashLInstalled: "安装完成",
|
|
905
1459
|
flashLUninstalled: "已卸载",
|
|
906
1460
|
flashLLaunched: "已启动",
|
|
907
1461
|
flashLFailed: "操作失败",
|
|
908
|
-
launcherPreviewHint: "
|
|
909
|
-
launcherUpdatePreview: "更新预览",
|
|
1462
|
+
launcherPreviewHint: "预览图会在安装时自动生成",
|
|
910
1463
|
launcherBadUrl: "链接非法(仅支持 http/https 直链)",
|
|
911
1464
|
launcherNoEntry: "未找到可执行入口"
|
|
912
1465
|
},
|
|
@@ -987,11 +1540,9 @@ window.__ModuleLoader__.load({
|
|
|
987
1540
|
manage: "Manage",
|
|
988
1541
|
manageDone: "Done",
|
|
989
1542
|
openSource: "Open source file",
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
detailAddr: "Location",
|
|
994
|
-
detailExe: "Executable",
|
|
1543
|
+
launcherColumnName: "Name",
|
|
1544
|
+
launcherColumnLocation: "Location",
|
|
1545
|
+
launcherColumnDownloadedAt: "Download time",
|
|
995
1546
|
launcherManageHint: "Launch and uninstall from Library → Apps.",
|
|
996
1547
|
confirmUninstallTitle: "Uninstall?",
|
|
997
1548
|
confirmUninstallBody: "This deletes the local files of (DWP wallpapers can be reinstalled from the market):",
|
|
@@ -1064,7 +1615,7 @@ window.__ModuleLoader__.load({
|
|
|
1064
1615
|
launcherTutorialPrep3Lead: "Click “",
|
|
1065
1616
|
launcherTutorialPrep3Tail: "” (the quoted text is the link)",
|
|
1066
1617
|
launcherTutorialUseTitle: "Daily usage:",
|
|
1067
|
-
launcherTutorialUse1: "1.
|
|
1618
|
+
launcherTutorialUse1: "1. Paste the whole share post into the box above (auto-parsed), or fill in link, archive password, passcode and title directly",
|
|
1068
1619
|
launcherTutorialUse2: "2. Click \"Install\"",
|
|
1069
1620
|
launcherTutorialUse3: "3. Once the app tile appears in the library, click it to launch (a confirm dialog shows each time)",
|
|
1070
1621
|
launcherAuthPlaceholder: "Basic xxxx… or basic:phone:token",
|
|
@@ -1089,6 +1640,21 @@ window.__ModuleLoader__.load({
|
|
|
1089
1640
|
launcherShareFail: "Share resolve failed (see server detail in brackets)",
|
|
1090
1641
|
launcherShareCode139: "This 139 share needs a passcode — enter it in the passcode box and retry",
|
|
1091
1642
|
launcherShareCode139Wrong: "Wrong 139 passcode — check it and retry",
|
|
1643
|
+
launcherSmartPasteLabel: "Paste a link or the full share text",
|
|
1644
|
+
launcherSmartPastePlaceholder: "Paste the whole share post. The link, passcode, archive password and launcher file are detected automatically.",
|
|
1645
|
+
launcherSmartPasteParse: "Parse",
|
|
1646
|
+
launcherSmartPasteReparse: "Re-parse",
|
|
1647
|
+
launcherSmartPasteClear: "Clear",
|
|
1648
|
+
launcherSmartPasteDone: "Detected {n} item(s): {items} (you can still edit them below)",
|
|
1649
|
+
launcherSmartPasteLow: "; {items} is low confidence — please double-check",
|
|
1650
|
+
launcherSmartPasteNone: "No install info detected in the pasted text — fill the fields manually",
|
|
1651
|
+
launcherSmartPasteKept: "Info detected, but your manual input was kept (click \"Parse\" to overwrite)",
|
|
1652
|
+
launcherSmartPasteKeptDetail: "Detected {items}, but your manual input was kept (click \"Parse\" to overwrite)",
|
|
1653
|
+
launcherSmartPasteSkipNote: "; kept your manual input for {items} (click \"Parse\" to overwrite)",
|
|
1654
|
+
launcherFieldUrl: "link",
|
|
1655
|
+
launcherFieldCode: "passcode",
|
|
1656
|
+
launcherFieldPwd: "archive password",
|
|
1657
|
+
launcherFieldExe: "launcher file",
|
|
1092
1658
|
launcherInstall: "Download & Install",
|
|
1093
1659
|
launcherInstalling: "Downloading…",
|
|
1094
1660
|
launcherEmpty: "No apps installed yet. Paste a direct link and click \"Download & Install\".",
|
|
@@ -1097,21 +1663,15 @@ window.__ModuleLoader__.load({
|
|
|
1097
1663
|
launcherLaunching: "Launching…",
|
|
1098
1664
|
launcherOpenFolder: "Open folder",
|
|
1099
1665
|
launcherUninstall: "Uninstall",
|
|
1100
|
-
launcherSource: "Source",
|
|
1101
|
-
launcherSha: "SHA512",
|
|
1102
|
-
launcherEntry: "Entry",
|
|
1103
1666
|
launcherConfirmTitle: "Launch this program?",
|
|
1104
1667
|
launcherConfirmBody: "The executable below will be started. Make sure you trust its source:",
|
|
1105
1668
|
launcherConfirmGo: "Launch",
|
|
1106
1669
|
launcherConfirmCancel: "Cancel",
|
|
1107
|
-
launcherSetEntry: "Set as entry",
|
|
1108
|
-
launcherCandidates: "Multiple executables found. Current entry:",
|
|
1109
1670
|
flashLInstalled: "Installed",
|
|
1110
1671
|
flashLUninstalled: "Uninstalled",
|
|
1111
1672
|
flashLLaunched: "Launched",
|
|
1112
1673
|
flashLFailed: "Operation failed",
|
|
1113
|
-
launcherPreviewHint: "
|
|
1114
|
-
launcherUpdatePreview: "Update preview",
|
|
1674
|
+
launcherPreviewHint: "The preview is generated automatically during installation",
|
|
1115
1675
|
launcherBadUrl: "Invalid link (http/https direct links only)",
|
|
1116
1676
|
launcherNoEntry: "No executable entry found"
|
|
1117
1677
|
}
|
|
@@ -1516,6 +2076,26 @@ window.__ModuleLoader__.load({
|
|
|
1516
2076
|
const [lPwdErr, setLPwdErr] = (0, react.useState)(false);
|
|
1517
2077
|
const [lCode, setLCode] = (0, react.useState)("");
|
|
1518
2078
|
const [lCodeErr, setLCodeErr] = (0, react.useState)(false);
|
|
2079
|
+
const [lPaste, setLPaste] = (0, react.useState)("");
|
|
2080
|
+
const [lPasteHint, setLPasteHint] = (0, react.useState)("");
|
|
2081
|
+
const [lParsed, setLParsed] = (0, react.useState)(false);
|
|
2082
|
+
/** 用户手改过的字段:普通解析不覆盖它们,只有「重新解析」强制覆盖(见 runSmartParse) */
|
|
2083
|
+
const lManual = (0, react.useRef)({
|
|
2084
|
+
url: false,
|
|
2085
|
+
title: false,
|
|
2086
|
+
pwd: false,
|
|
2087
|
+
code: false
|
|
2088
|
+
});
|
|
2089
|
+
(0, react.useEffect)(() => {
|
|
2090
|
+
if (lPaste.trim() === "") return;
|
|
2091
|
+
const id = window.setTimeout(() => {
|
|
2092
|
+
runSmartParse(lPaste, false);
|
|
2093
|
+
setLParsed(true);
|
|
2094
|
+
}, 500);
|
|
2095
|
+
return () => {
|
|
2096
|
+
window.clearTimeout(id);
|
|
2097
|
+
};
|
|
2098
|
+
}, [lPaste]);
|
|
1519
2099
|
const [lAuthOpen, setLAuthOpen] = (0, react.useState)(false);
|
|
1520
2100
|
const [lAuth, setLAuth] = (0, react.useState)("");
|
|
1521
2101
|
const [lAuthPresent, setLAuthPresent] = (0, react.useState)("");
|
|
@@ -1529,25 +2109,6 @@ window.__ModuleLoader__.load({
|
|
|
1529
2109
|
const [lFlash, setLFlash] = (0, react.useState)("");
|
|
1530
2110
|
const [lSearch, setLSearch] = (0, react.useState)("");
|
|
1531
2111
|
const [lConfirm, setLConfirm] = (0, react.useState)(null);
|
|
1532
|
-
const [lEntryFor, setLEntryFor] = (0, react.useState)(null);
|
|
1533
|
-
const [lDetailFor, setLDetailFor] = (0, react.useState)(null);
|
|
1534
|
-
const [lChoices, setLChoices] = (0, react.useState)({});
|
|
1535
|
-
(0, react.useEffect)(() => {
|
|
1536
|
-
if (lDetailFor === null) return;
|
|
1537
|
-
const raf = requestAnimationFrame(() => {
|
|
1538
|
-
const host = panelRef.current;
|
|
1539
|
-
const vp = viewportRef.current;
|
|
1540
|
-
if (host === null || vp === null) return;
|
|
1541
|
-
const el = Array.from(host.querySelectorAll("[data-wesync-detail]")).find((x) => x.getAttribute("data-wesync-detail") === lDetailFor) ?? null;
|
|
1542
|
-
if (el === null) return;
|
|
1543
|
-
measure();
|
|
1544
|
-
const overflow = el.getBoundingClientRect().bottom - vp.getBoundingClientRect().bottom + 12;
|
|
1545
|
-
if (overflow <= 0) return;
|
|
1546
|
-
const r = rangeOf(pageIdx("library"));
|
|
1547
|
-
posRef.current = Math.max(r.top, Math.min(r.bottom, posRef.current + overflow));
|
|
1548
|
-
});
|
|
1549
|
-
return () => cancelAnimationFrame(raf);
|
|
1550
|
-
}, [lDetailFor]);
|
|
1551
2112
|
(0, react.useEffect)(() => store.subscribe(() => {
|
|
1552
2113
|
setInfo(store.info);
|
|
1553
2114
|
setEnabled(store.settings.enabled);
|
|
@@ -1966,6 +2527,68 @@ window.__ModuleLoader__.load({
|
|
|
1966
2527
|
return "";
|
|
1967
2528
|
}
|
|
1968
2529
|
};
|
|
2530
|
+
const runSmartParse = (raw, force) => {
|
|
2531
|
+
if (raw.trim() === "") {
|
|
2532
|
+
setLPasteHint("");
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
const info = parseInstallShareText(raw);
|
|
2536
|
+
const manual = lManual.current;
|
|
2537
|
+
const filled = [];
|
|
2538
|
+
const low = [];
|
|
2539
|
+
const skipped = [];
|
|
2540
|
+
const put = (label, value, confidence, dirty, apply) => {
|
|
2541
|
+
if (value === void 0 || confidence < .5) return false;
|
|
2542
|
+
if (!force && dirty) {
|
|
2543
|
+
skipped.push(label);
|
|
2544
|
+
return false;
|
|
2545
|
+
}
|
|
2546
|
+
apply(value);
|
|
2547
|
+
filled.push(label);
|
|
2548
|
+
if (confidence < .8) low.push(label);
|
|
2549
|
+
return true;
|
|
2550
|
+
};
|
|
2551
|
+
put(t.launcherFieldUrl, info.sourceUrl, info.confidence.sourceUrl, manual.url, setLUrl);
|
|
2552
|
+
const setCode = put(t.launcherFieldCode, info.shareCode, info.confidence.shareCode, manual.code, setLCode);
|
|
2553
|
+
const setPwd = put(t.launcherFieldPwd, info.archivePassword, info.confidence.archivePassword, manual.pwd, setLPwd);
|
|
2554
|
+
put(t.launcherFieldExe, info.executableName, info.confidence.executableName, manual.title, setLTitle);
|
|
2555
|
+
if (force) lManual.current = {
|
|
2556
|
+
url: false,
|
|
2557
|
+
title: false,
|
|
2558
|
+
pwd: false,
|
|
2559
|
+
code: false
|
|
2560
|
+
};
|
|
2561
|
+
if (setPwd) setLPwdErr(false);
|
|
2562
|
+
if (setCode) setLCodeErr(false);
|
|
2563
|
+
if (filled.length === 0) {
|
|
2564
|
+
setLPasteHint(info.matched > 0 ? skipped.length > 0 ? t.launcherSmartPasteKeptDetail.replace("{items}", skipped.join(" / ")) : t.launcherSmartPasteKept : t.launcherSmartPasteNone);
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
let hint = t.launcherSmartPasteDone.replace("{n}", String(filled.length)).replace("{items}", filled.join(" / "));
|
|
2568
|
+
if (low.length > 0) hint += t.launcherSmartPasteLow.replace("{items}", low.join(" / "));
|
|
2569
|
+
if (skipped.length > 0) hint += t.launcherSmartPasteSkipNote.replace("{items}", skipped.join(" / "));
|
|
2570
|
+
setLPasteHint(hint);
|
|
2571
|
+
};
|
|
2572
|
+
/**
|
|
2573
|
+
* 点「解析 / 重新解析」:**显式点击 = 用户同意覆盖**,直接强制填入识别到的字段。
|
|
2574
|
+
* 与被动解析(粘贴 / 停止输入 500ms)区分开:被动的那些绝不动用户手改过的字段
|
|
2575
|
+
* (需求第十四节),而点按钮是明确的一次操作,这正是该节里"再次点击重新解析可覆盖"的语义。
|
|
2576
|
+
*/
|
|
2577
|
+
const onSmartParseClick = () => {
|
|
2578
|
+
runSmartParse(lPaste, true);
|
|
2579
|
+
setLParsed(true);
|
|
2580
|
+
};
|
|
2581
|
+
/** 粘贴即自动解析(只针对本次粘贴的内容;解析后仍可继续编辑文本框,不破坏编辑能力)。 */
|
|
2582
|
+
const onSmartPaste = (e) => {
|
|
2583
|
+
const text = e.clipboardData?.getData("text") ?? "";
|
|
2584
|
+
if (text.trim() === "") return;
|
|
2585
|
+
runSmartParse(text, false);
|
|
2586
|
+
setLParsed(true);
|
|
2587
|
+
};
|
|
2588
|
+
const onSmartPasteClear = () => {
|
|
2589
|
+
setLPaste("");
|
|
2590
|
+
setLPasteHint("");
|
|
2591
|
+
};
|
|
1969
2592
|
const onLauncherInstall = async () => {
|
|
1970
2593
|
const url = lUrl.trim();
|
|
1971
2594
|
if (!isValidHttpUrl(url)) {
|
|
@@ -2012,19 +2635,21 @@ window.__ModuleLoader__.load({
|
|
|
2012
2635
|
flashLErr(t.flashLFailed + (out.error !== void 0 ? ":" + out.error : ""));
|
|
2013
2636
|
return;
|
|
2014
2637
|
}
|
|
2015
|
-
if (out.record !== void 0 && (out.candidates?.length ?? 0) > 1) {
|
|
2016
|
-
setLChoices((m) => ({
|
|
2017
|
-
...m,
|
|
2018
|
-
[out.record.id]: out.candidates
|
|
2019
|
-
}));
|
|
2020
|
-
setLEntryFor(out.record.id);
|
|
2021
|
-
}
|
|
2022
2638
|
setLUrl("");
|
|
2023
2639
|
setLTitle("");
|
|
2024
2640
|
setLPwd("");
|
|
2025
2641
|
setLPwdErr(false);
|
|
2026
2642
|
setLCode("");
|
|
2027
2643
|
setLCodeErr(false);
|
|
2644
|
+
setLPaste("");
|
|
2645
|
+
setLPasteHint("");
|
|
2646
|
+
setLParsed(false);
|
|
2647
|
+
lManual.current = {
|
|
2648
|
+
url: false,
|
|
2649
|
+
title: false,
|
|
2650
|
+
pwd: false,
|
|
2651
|
+
code: false
|
|
2652
|
+
};
|
|
2028
2653
|
flashL(t.flashLInstalled);
|
|
2029
2654
|
loadLauncher();
|
|
2030
2655
|
loadApps();
|
|
@@ -2071,32 +2696,6 @@ window.__ModuleLoader__.load({
|
|
|
2071
2696
|
if (r.ok) flashL(t.flashLLaunched + ":" + rec.title);
|
|
2072
2697
|
else flashLErr(t.flashLFailed + (r.error !== void 0 ? ":" + r.error : ""));
|
|
2073
2698
|
};
|
|
2074
|
-
const onUpdatePreview = async (rec) => {
|
|
2075
|
-
const dataUrl = makeLauncherCard(rec.title);
|
|
2076
|
-
if (dataUrl === "") {
|
|
2077
|
-
flashL(t.flashLFailed);
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
if ((await fetch("/we-sync/launcher/preview", {
|
|
2081
|
-
method: "POST",
|
|
2082
|
-
headers: { "Content-Type": "application/json" },
|
|
2083
|
-
body: JSON.stringify({
|
|
2084
|
-
id: rec.id,
|
|
2085
|
-
dataUrl
|
|
2086
|
-
})
|
|
2087
|
-
})).ok) {
|
|
2088
|
-
flashL(t.launcherUpdatePreview + " ✓");
|
|
2089
|
-
loadApps();
|
|
2090
|
-
} else flashL(t.flashLFailed);
|
|
2091
|
-
};
|
|
2092
|
-
const onSetEntry = async (rec, file) => {
|
|
2093
|
-
const r = await setEntry(rec.id, file, (u, i) => fetch(u, i));
|
|
2094
|
-
if (r.ok) {
|
|
2095
|
-
flashL(t.launcherEntry + " → " + file);
|
|
2096
|
-
setLEntryFor(null);
|
|
2097
|
-
loadApps();
|
|
2098
|
-
} else flashL(t.flashLFailed + (r.error !== void 0 ? ":" + r.error : ""));
|
|
2099
|
-
};
|
|
2100
2699
|
const loadDirs = async () => {
|
|
2101
2700
|
try {
|
|
2102
2701
|
const body = await (await fetch("/we-sync/apps/dirs", { cache: "no-store" })).json();
|
|
@@ -2926,6 +3525,46 @@ window.__ModuleLoader__.load({
|
|
|
2926
3525
|
})
|
|
2927
3526
|
})
|
|
2928
3527
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3528
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3529
|
+
className: "wesync-paste",
|
|
3530
|
+
children: [
|
|
3531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3532
|
+
className: "wesync-paste-head",
|
|
3533
|
+
children: [
|
|
3534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3535
|
+
className: "wesync-paste-label",
|
|
3536
|
+
children: t.launcherSmartPasteLabel
|
|
3537
|
+
}),
|
|
3538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3539
|
+
className: "wesync-btn wesync-paste-btn",
|
|
3540
|
+
disabled: lPaste.trim() === "",
|
|
3541
|
+
onClick: onSmartParseClick,
|
|
3542
|
+
children: lParsed ? t.launcherSmartPasteReparse : t.launcherSmartPasteParse
|
|
3543
|
+
}),
|
|
3544
|
+
lPaste !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3545
|
+
className: "wesync-btn wesync-paste-btn",
|
|
3546
|
+
onClick: onSmartPasteClear,
|
|
3547
|
+
children: t.launcherSmartPasteClear
|
|
3548
|
+
}) : null
|
|
3549
|
+
]
|
|
3550
|
+
}),
|
|
3551
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3552
|
+
className: "wesync-paste-area",
|
|
3553
|
+
rows: 3,
|
|
3554
|
+
spellCheck: false,
|
|
3555
|
+
placeholder: t.launcherSmartPastePlaceholder,
|
|
3556
|
+
value: lPaste,
|
|
3557
|
+
onChange: (e) => {
|
|
3558
|
+
setLPaste(e.target.value);
|
|
3559
|
+
},
|
|
3560
|
+
onPaste: onSmartPaste
|
|
3561
|
+
}),
|
|
3562
|
+
lPasteHint !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3563
|
+
className: "wesync-paste-hint",
|
|
3564
|
+
children: lPasteHint
|
|
3565
|
+
}) : null
|
|
3566
|
+
]
|
|
3567
|
+
}),
|
|
2929
3568
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2930
3569
|
className: "wesync-dir-row",
|
|
2931
3570
|
children: [
|
|
@@ -2933,7 +3572,10 @@ window.__ModuleLoader__.load({
|
|
|
2933
3572
|
className: "wesync-dir-input",
|
|
2934
3573
|
placeholder: t.launcherUrlPlaceholder,
|
|
2935
3574
|
value: lUrl,
|
|
2936
|
-
onChange: (e) =>
|
|
3575
|
+
onChange: (e) => {
|
|
3576
|
+
lManual.current.url = true;
|
|
3577
|
+
setLUrl(e.target.value);
|
|
3578
|
+
},
|
|
2937
3579
|
onKeyDown: (e) => {
|
|
2938
3580
|
if (e.key === "Enter" && !lBusy) onLauncherInstall();
|
|
2939
3581
|
}
|
|
@@ -2943,7 +3585,10 @@ window.__ModuleLoader__.load({
|
|
|
2943
3585
|
style: { maxWidth: 180 },
|
|
2944
3586
|
placeholder: t.launcherTitlePlaceholder,
|
|
2945
3587
|
value: lTitle,
|
|
2946
|
-
onChange: (e) =>
|
|
3588
|
+
onChange: (e) => {
|
|
3589
|
+
lManual.current.title = true;
|
|
3590
|
+
setLTitle(e.target.value);
|
|
3591
|
+
}
|
|
2947
3592
|
}),
|
|
2948
3593
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2949
3594
|
className: "wesync-dir-input",
|
|
@@ -2955,6 +3600,7 @@ window.__ModuleLoader__.load({
|
|
|
2955
3600
|
placeholder: t.launcherPwdPlaceholder,
|
|
2956
3601
|
value: lPwd,
|
|
2957
3602
|
onChange: (e) => {
|
|
3603
|
+
lManual.current.pwd = true;
|
|
2958
3604
|
setLPwd(e.target.value);
|
|
2959
3605
|
if (lPwdErr) setLPwdErr(false);
|
|
2960
3606
|
},
|
|
@@ -2971,6 +3617,7 @@ window.__ModuleLoader__.load({
|
|
|
2971
3617
|
placeholder: t.launcherCodePlaceholder,
|
|
2972
3618
|
value: lCode,
|
|
2973
3619
|
onChange: (e) => {
|
|
3620
|
+
lManual.current.code = true;
|
|
2974
3621
|
setLCode(e.target.value);
|
|
2975
3622
|
if (lCodeErr) setLCodeErr(false);
|
|
2976
3623
|
},
|
|
@@ -3124,118 +3771,36 @@ window.__ModuleLoader__.load({
|
|
|
3124
3771
|
className: "wesync-app-empty",
|
|
3125
3772
|
children: t.launcherNoMatch
|
|
3126
3773
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3127
|
-
className: "wesync-
|
|
3128
|
-
children:
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
rec.file
|
|
3158
|
-
]
|
|
3159
|
-
}),
|
|
3160
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3161
|
-
className: "wesync-market-actions",
|
|
3162
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3163
|
-
className: "wesync-btn wesync-detail-btn",
|
|
3164
|
-
onClick: () => {
|
|
3165
|
-
setLDetailFor(lDetailFor === rec.id ? null : rec.id);
|
|
3166
|
-
},
|
|
3167
|
-
children: lDetailFor === rec.id ? t.detailHide : t.detail
|
|
3168
|
-
})
|
|
3169
|
-
}),
|
|
3170
|
-
lDetailFor === rec.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3171
|
-
className: "wesync-market-meta",
|
|
3172
|
-
"data-wesync-detail": rec.id,
|
|
3173
|
-
style: { width: "100%" },
|
|
3174
|
-
children: [
|
|
3175
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
3176
|
-
t.detailInstalledAt,
|
|
3177
|
-
": ",
|
|
3178
|
-
formatInstalledAt(rec.installedAt)
|
|
3179
|
-
] }),
|
|
3180
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
3181
|
-
t.detailAddr,
|
|
3182
|
-
": ",
|
|
3183
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3184
|
-
style: { wordBreak: "break-all" },
|
|
3185
|
-
children: launcherAppDir(lRoot, rec.slug)
|
|
3186
|
-
})
|
|
3187
|
-
] }),
|
|
3188
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
3189
|
-
t.detailExe,
|
|
3190
|
-
": ",
|
|
3191
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3192
|
-
style: { wordBreak: "break-all" },
|
|
3193
|
-
children: rec.file
|
|
3194
|
-
})
|
|
3195
|
-
] }),
|
|
3196
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
3197
|
-
t.launcherSource,
|
|
3198
|
-
": ",
|
|
3199
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3200
|
-
style: { wordBreak: "break-all" },
|
|
3201
|
-
children: rec.sourceUrl
|
|
3202
|
-
})
|
|
3203
|
-
] }),
|
|
3204
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
3205
|
-
t.launcherSha,
|
|
3206
|
-
": ",
|
|
3207
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3208
|
-
style: { wordBreak: "break-all" },
|
|
3209
|
-
children: [rec.sha512.slice(0, 32), "…"]
|
|
3210
|
-
})
|
|
3211
|
-
] }),
|
|
3212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3213
|
-
className: "wesync-btn",
|
|
3214
|
-
style: { marginTop: 4 },
|
|
3215
|
-
onClick: () => {
|
|
3216
|
-
onUpdatePreview(rec);
|
|
3217
|
-
},
|
|
3218
|
-
children: t.launcherUpdatePreview
|
|
3219
|
-
}),
|
|
3220
|
-
candidates.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3221
|
-
style: { marginTop: 6 },
|
|
3222
|
-
children: t.launcherCandidates
|
|
3223
|
-
}), candidates.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3224
|
-
className: "wesync-btn",
|
|
3225
|
-
style: { margin: "2px 4px 0 0" },
|
|
3226
|
-
onClick: () => {
|
|
3227
|
-
onSetEntry(rec, f);
|
|
3228
|
-
},
|
|
3229
|
-
children: [
|
|
3230
|
-
f === rec.file ? "● " : "",
|
|
3231
|
-
f,
|
|
3232
|
-
f === rec.file ? "" : " → " + t.launcherSetEntry
|
|
3233
|
-
]
|
|
3234
|
-
}, f))] }) : null
|
|
3235
|
-
]
|
|
3236
|
-
}) : null
|
|
3237
|
-
]
|
|
3238
|
-
}, rec.id);
|
|
3774
|
+
className: "wesync-launcher-table-wrap",
|
|
3775
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
3776
|
+
className: "wesync-launcher-table",
|
|
3777
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3778
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
3779
|
+
scope: "col",
|
|
3780
|
+
children: t.launcherColumnName
|
|
3781
|
+
}),
|
|
3782
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
3783
|
+
scope: "col",
|
|
3784
|
+
children: t.launcherColumnLocation
|
|
3785
|
+
}),
|
|
3786
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
3787
|
+
scope: "col",
|
|
3788
|
+
children: t.launcherColumnDownloadedAt
|
|
3789
|
+
})
|
|
3790
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: filteredL.map((rec) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3792
|
+
className: "wesync-launcher-name-cell",
|
|
3793
|
+
children: rec.title
|
|
3794
|
+
}),
|
|
3795
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3796
|
+
className: "wesync-launcher-location",
|
|
3797
|
+
children: launcherAppDir(lRoot, rec.slug)
|
|
3798
|
+
}),
|
|
3799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3800
|
+
className: "wesync-launcher-downloaded",
|
|
3801
|
+
children: formatInstalledAt(rec.installedAt)
|
|
3802
|
+
})
|
|
3803
|
+
] }, rec.id)) })]
|
|
3239
3804
|
})
|
|
3240
3805
|
})
|
|
3241
3806
|
] });
|
|
@@ -3933,14 +4498,6 @@ body[data-ds-dark-theme] .wesync-gaze-status.is-error { color: #fdba74; }
|
|
|
3933
4498
|
margin: 6px 0 8px;
|
|
3934
4499
|
}
|
|
3935
4500
|
|
|
3936
|
-
/* 应用启动器卡片:操作行只剩「详细」,撑满整行——点击区域大,
|
|
3937
|
-
不再是一排挤在卡片底部、字号 11px 的小按钮(原先「启动 / … / 卸载」三个)。 */
|
|
3938
|
-
.wesync-detail-btn {
|
|
3939
|
-
flex: 1;
|
|
3940
|
-
font-size: 12px;
|
|
3941
|
-
padding: 6px 10px;
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
4501
|
/* 壁纸库「本地」栏 · 管理模式:卡片上的操作行(打开源文件 / 卸载)。
|
|
3945
4502
|
只在「管理」开启时渲染,平时卡片上没有任何小按钮。 */
|
|
3946
4503
|
.wesync-manage-row {
|
|
@@ -4195,6 +4752,59 @@ body[data-ds-dark-theme] .wesync-gaze-status.is-error { color: #fdba74; }
|
|
|
4195
4752
|
color: var(--dsw-alias-label-secondary);
|
|
4196
4753
|
}
|
|
4197
4754
|
|
|
4755
|
+
/* ── 应用启动器 · Smart Paste(整段分享文本 → 自动解析进下面四个字段)──────
|
|
4756
|
+
不是替代品:原文留在文本框里可继续编辑,解析结果仍可在输入框微调。 */
|
|
4757
|
+
.wesync-paste {
|
|
4758
|
+
display: flex;
|
|
4759
|
+
flex-direction: column;
|
|
4760
|
+
gap: 6px;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
.wesync-paste-head {
|
|
4764
|
+
display: flex;
|
|
4765
|
+
align-items: center;
|
|
4766
|
+
gap: 8px;
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
.wesync-paste-label {
|
|
4770
|
+
flex: 1;
|
|
4771
|
+
min-width: 0;
|
|
4772
|
+
font-size: 12px;
|
|
4773
|
+
color: var(--dsw-alias-label-secondary);
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
.wesync-paste-btn {
|
|
4777
|
+
flex: 0 0 auto;
|
|
4778
|
+
font-size: 12px;
|
|
4779
|
+
padding: 4px 12px;
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
.wesync-paste-area {
|
|
4783
|
+
width: 100%;
|
|
4784
|
+
box-sizing: border-box;
|
|
4785
|
+
min-height: 62px;
|
|
4786
|
+
max-height: 220px;
|
|
4787
|
+
padding: 8px 10px;
|
|
4788
|
+
border-radius: 8px;
|
|
4789
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
4790
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
4791
|
+
color: var(--dsw-alias-label-primary);
|
|
4792
|
+
font-size: 12px;
|
|
4793
|
+
font-family: inherit;
|
|
4794
|
+
line-height: 1.6;
|
|
4795
|
+
resize: vertical;
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
.wesync-paste-area::placeholder {
|
|
4799
|
+
color: var(--dsw-alias-label-secondary);
|
|
4800
|
+
opacity: 0.7;
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
.wesync-paste-hint {
|
|
4804
|
+
font-size: 12px;
|
|
4805
|
+
color: var(--dsw-alias-label-secondary);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4198
4808
|
/* 壁纸库「市场」一栏:flash 提示 + 卡片元信息 + 安装/卸载按钮 */
|
|
4199
4809
|
.wesync-market-flash {
|
|
4200
4810
|
font-size: 12px; padding: 6px 10px; border-radius: 8px;
|
|
@@ -4207,6 +4817,45 @@ body[data-ds-dark-theme] .wesync-gaze-status.is-error { color: #fdba74; }
|
|
|
4207
4817
|
.wesync-market-install, .wesync-market-uninstall { flex: 1; font-size: 12px; padding: 6px 8px; }
|
|
4208
4818
|
.wesync-market-uninstall { opacity: 0.8; }
|
|
4209
4819
|
|
|
4820
|
+
/* 壁纸库 · 应用启动器:名称、安装位置与下载时间按行对齐。 */
|
|
4821
|
+
.wesync-launcher-table-wrap {
|
|
4822
|
+
width: 100%;
|
|
4823
|
+
overflow-x: auto;
|
|
4824
|
+
margin-top: 8px;
|
|
4825
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
4826
|
+
border-radius: 8px;
|
|
4827
|
+
}
|
|
4828
|
+
.wesync-launcher-table {
|
|
4829
|
+
width: 100%;
|
|
4830
|
+
border-collapse: collapse;
|
|
4831
|
+
table-layout: fixed;
|
|
4832
|
+
font-size: 12px;
|
|
4833
|
+
}
|
|
4834
|
+
.wesync-launcher-table th,
|
|
4835
|
+
.wesync-launcher-table td {
|
|
4836
|
+
padding: 8px 10px;
|
|
4837
|
+
text-align: left;
|
|
4838
|
+
vertical-align: top;
|
|
4839
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
4840
|
+
overflow-wrap: anywhere;
|
|
4841
|
+
}
|
|
4842
|
+
.wesync-launcher-table th {
|
|
4843
|
+
color: var(--dsw-alias-label-secondary);
|
|
4844
|
+
font-size: 11px;
|
|
4845
|
+
font-weight: 600;
|
|
4846
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
4847
|
+
}
|
|
4848
|
+
.wesync-launcher-table th:nth-child(1),
|
|
4849
|
+
.wesync-launcher-table td:nth-child(1) { width: 30%; }
|
|
4850
|
+
.wesync-launcher-table th:nth-child(2),
|
|
4851
|
+
.wesync-launcher-table td:nth-child(2) { width: 44%; }
|
|
4852
|
+
.wesync-launcher-table th:nth-child(3),
|
|
4853
|
+
.wesync-launcher-table td:nth-child(3) { width: 26%; }
|
|
4854
|
+
.wesync-launcher-table tbody tr:hover { background: var(--dsw-alias-bg-overlay); }
|
|
4855
|
+
.wesync-launcher-name-cell { color: var(--dsw-alias-label-primary); }
|
|
4856
|
+
.wesync-launcher-location { color: var(--dsw-alias-label-secondary); }
|
|
4857
|
+
.wesync-launcher-downloaded { color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
|
|
4858
|
+
|
|
4210
4859
|
/* ── 应用启动器:确认弹层(每次启动都要用户手势确认)────────────────── */
|
|
4211
4860
|
.wesync-confirm-mask {
|
|
4212
4861
|
position: fixed;
|
|
@@ -11440,7 +12089,7 @@ void main(){
|
|
|
11440
12089
|
*/
|
|
11441
12090
|
const inject = ["slots", "theme"];
|
|
11442
12091
|
/** 插件版本:构建期由 tsdown 的 define 从 package.json 注入(未注入的裸跑为 'dev')。 */
|
|
11443
|
-
const PLUGIN_VERSION = "26.9.
|
|
12092
|
+
const PLUGIN_VERSION = "26.9.29";
|
|
11444
12093
|
/** 插件 GitHub 仓库主页(版本号角标点击跳转)。 */
|
|
11445
12094
|
const PLUGIN_REPO_URL = "https://github.com/YRN-playmaker/dsh-wallpaper_share";
|
|
11446
12095
|
/** 专注模式:任务进行中(本版下调的全局值;鼠标圆内另按 FOCUS_LENS 加浓) */
|