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/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
- detail: "详细",
787
- detailHide: "收起",
788
- detailInstalledAt: "安装时间",
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
- detail: "Details",
991
- detailHide: "Hide",
992
- detailInstalledAt: "Installed",
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. Fill in the download link, archive password, share passcode and title in the input row",
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: "Preview: regenerate via \"Update preview\" below after install",
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) => setLUrl(e.target.value),
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) => setLTitle(e.target.value)
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-apps-grid",
3128
- children: filteredL.map((rec) => {
3129
- const candidates = lChoices[rec.id] ?? [];
3130
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3131
- className: "wesync-app-card wesync-market-card",
3132
- children: [
3133
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3134
- className: "wesync-app-thumbwrap",
3135
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3136
- className: "wesync-app-thumb",
3137
- src: "/we-sync/launcher/preview-file?id=" + encodeURIComponent(rec.id),
3138
- alt: rec.title,
3139
- loading: "lazy",
3140
- onError: (e) => {
3141
- e.currentTarget.style.visibility = "hidden";
3142
- }
3143
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3144
- className: "wesync-app-badge wesync-badge-launcher",
3145
- children: t.typeLauncherApp
3146
- })]
3147
- }),
3148
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3149
- className: "wesync-app-title",
3150
- children: rec.title
3151
- }),
3152
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3153
- className: "wesync-market-meta",
3154
- children: [
3155
- humanSize(rec.size),
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.13";
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 加浓) */