dsh-mobile 0.6.16 → 0.6.18
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/CHANGELOG.md +34 -10
- package/CONTRIBUTORS.md +28 -10
- package/README.en.md +16 -10
- package/README.md +15 -10
- package/SECURITY.md +1 -1
- package/cordis.patch.yml +6 -17
- package/docs/ATTACH_EXISTING_FRPS.en.md +32 -2
- package/docs/ATTACH_EXISTING_FRPS.md +2 -2
- package/docs/CLIENT_MODULES.en.md +10 -2
- package/docs/CLIENT_MODULES.md +10 -2
- package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +188 -0
- package/docs/MOBILE_PLUGIN_ADAPTATION.md +188 -0
- package/docs/README.md +8 -0
- package/docs/SELF_HOSTED_ORIGIN.en.md +1 -1
- package/docs/SELF_HOSTED_ORIGIN.md +2 -2
- package/docs/SLOW_CONNECTIONS.en.md +13 -3
- package/docs/SLOW_CONNECTIONS.md +13 -3
- package/docs/TRUSTED_NETWORKS.en.md +17 -0
- package/docs/TRUSTED_NETWORKS.md +17 -0
- package/lib/cli.js.map +1 -1
- package/lib/client.js +1310 -31
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +53 -1
- package/lib/index.mjs +686 -56
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6630,18 +6630,67 @@ window.__ModuleLoader__.load({
|
|
|
6630
6630
|
const omitted = !entry.required && excluded.has(entry.id);
|
|
6631
6631
|
return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
|
|
6632
6632
|
}
|
|
6633
|
+
/** Format a module's measured transfer size for the settings panel.
|
|
6634
|
+
* @param entry - Catalog row with optional measured sizes.
|
|
6635
|
+
* @returns A short size label, or `undefined` when nothing was measured.
|
|
6636
|
+
*/
|
|
6637
|
+
function formatClientModuleSize(entry) {
|
|
6638
|
+
const value = entry.gzipBytes ?? entry.bytes;
|
|
6639
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) return void 0;
|
|
6640
|
+
if (value === 0) return "0 KB";
|
|
6641
|
+
const kilobytes = value / 1024;
|
|
6642
|
+
return kilobytes >= 1024 ? `${(kilobytes / 1024).toFixed(1)} MB` : `${String(Math.max(1, Math.round(kilobytes)))} KB`;
|
|
6643
|
+
}
|
|
6644
|
+
/** Sort measured modules largest first, keeping unknown sizes last and ties stable by ID. */
|
|
6645
|
+
function sortClientModulesBySize(entries) {
|
|
6646
|
+
return [...entries].sort((left, right) => {
|
|
6647
|
+
const leftBytes = left.gzipBytes ?? left.bytes;
|
|
6648
|
+
const rightBytes = right.gzipBytes ?? right.bytes;
|
|
6649
|
+
if (leftBytes === void 0 && rightBytes !== void 0) return 1;
|
|
6650
|
+
if (rightBytes === void 0 && leftBytes !== void 0) return -1;
|
|
6651
|
+
return (rightBytes ?? 0) - (leftBytes ?? 0) || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
6652
|
+
});
|
|
6653
|
+
}
|
|
6654
|
+
/**
|
|
6655
|
+
* Choose the smallest loadable set: required entries plus everything they depend on.
|
|
6656
|
+
*
|
|
6657
|
+
* Excluding every optional entry naively would keep a required module pointing at
|
|
6658
|
+
* an excluded dependency, which the Gateway rejects on save. The closure keeps
|
|
6659
|
+
* those dependencies loaded instead.
|
|
6660
|
+
* @param entries - Full catalog with declared dependencies.
|
|
6661
|
+
* @returns IDs to exclude, in catalog order.
|
|
6662
|
+
*/
|
|
6663
|
+
function clientModuleMinimalExclusion(entries) {
|
|
6664
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6665
|
+
const keep = new Set(entries.filter((entry) => entry.required).map((entry) => entry.id));
|
|
6666
|
+
const pending = [...keep];
|
|
6667
|
+
while (pending.length > 0) {
|
|
6668
|
+
const entry = byId.get(pending.pop());
|
|
6669
|
+
if (entry === void 0) continue;
|
|
6670
|
+
for (const dependency of entry.dependencies) {
|
|
6671
|
+
if (keep.has(dependency)) continue;
|
|
6672
|
+
keep.add(dependency);
|
|
6673
|
+
pending.push(dependency);
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
return entries.filter((entry) => !keep.has(entry.id)).map((entry) => entry.id);
|
|
6677
|
+
}
|
|
6633
6678
|
/** Editable selections retain unknown server exclusions and never cascade dependencies. */
|
|
6634
6679
|
var ClientModuleDraft = class {
|
|
6635
|
-
entries;
|
|
6636
6680
|
initial;
|
|
6637
6681
|
excluded;
|
|
6638
6682
|
entriesById;
|
|
6683
|
+
catalogEntries;
|
|
6639
6684
|
constructor(entries, excludedIds) {
|
|
6640
|
-
this.
|
|
6685
|
+
this.catalogEntries = entries;
|
|
6641
6686
|
this.initial = new Set(excludedIds);
|
|
6642
6687
|
this.excluded = new Set(excludedIds);
|
|
6643
6688
|
this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6644
6689
|
}
|
|
6690
|
+
/** @returns The original catalog with its most recently measured metadata. */
|
|
6691
|
+
get entries() {
|
|
6692
|
+
return this.catalogEntries;
|
|
6693
|
+
}
|
|
6645
6694
|
/** @returns Number of changed loading choices, including hidden filtered rows. */
|
|
6646
6695
|
get changeCount() {
|
|
6647
6696
|
let count = 0;
|
|
@@ -6689,13 +6738,60 @@ window.__ModuleLoader__.load({
|
|
|
6689
6738
|
if (loaded) this.excluded.delete(id);
|
|
6690
6739
|
else this.excluded.add(id);
|
|
6691
6740
|
}
|
|
6741
|
+
/** @returns Bytes the current selection still downloads, when sizes were measured. */
|
|
6742
|
+
loadedTransferBytes() {
|
|
6743
|
+
let total = 0;
|
|
6744
|
+
for (const entry of this.entries) {
|
|
6745
|
+
if (!this.isLoaded(entry.id)) continue;
|
|
6746
|
+
total += entry.gzipBytes ?? entry.bytes ?? 0;
|
|
6747
|
+
}
|
|
6748
|
+
return total;
|
|
6749
|
+
}
|
|
6750
|
+
/** @returns Number of selected modules whose transfer size is still unknown. */
|
|
6751
|
+
loadedUnknownSizeCount() {
|
|
6752
|
+
return this.entries.filter((entry) => this.isLoaded(entry.id) && entry.gzipBytes === void 0 && entry.bytes === void 0).length;
|
|
6753
|
+
}
|
|
6754
|
+
/** Refresh metadata without replacing saved or unsaved loading choices. */
|
|
6755
|
+
updateMeasurements(entries) {
|
|
6756
|
+
const measured = new Map(entries.map((entry) => [entry.id, entry]));
|
|
6757
|
+
for (const entry of this.entries) {
|
|
6758
|
+
const next = measured.get(entry.id);
|
|
6759
|
+
if (next === void 0) continue;
|
|
6760
|
+
const refreshed = { ...entry };
|
|
6761
|
+
delete refreshed.bytes;
|
|
6762
|
+
delete refreshed.gzipBytes;
|
|
6763
|
+
delete refreshed.description;
|
|
6764
|
+
this.entriesById.set(entry.id, {
|
|
6765
|
+
...refreshed,
|
|
6766
|
+
...next.bytes === void 0 ? {} : { bytes: next.bytes },
|
|
6767
|
+
...next.gzipBytes === void 0 ? {} : { gzipBytes: next.gzipBytes },
|
|
6768
|
+
...next.description === void 0 ? {} : { description: next.description }
|
|
6769
|
+
});
|
|
6770
|
+
}
|
|
6771
|
+
this.catalogEntries = this.entries.map((entry) => this.entriesById.get(entry.id));
|
|
6772
|
+
}
|
|
6773
|
+
/** Exclude every optional entry except dependencies a required entry still needs.
|
|
6774
|
+
* @returns Whether this draft changed; saving remains a separate action.
|
|
6775
|
+
*/
|
|
6776
|
+
applyMinimalSelection() {
|
|
6777
|
+
const target = new Set(clientModuleMinimalExclusion(this.entries));
|
|
6778
|
+
let changed = false;
|
|
6779
|
+
for (const entry of this.entries) {
|
|
6780
|
+
if (entry.required) continue;
|
|
6781
|
+
const shouldExclude = target.has(entry.id);
|
|
6782
|
+
if (shouldExclude !== this.excluded.has(entry.id)) changed = true;
|
|
6783
|
+
if (shouldExclude) this.excluded.add(entry.id);
|
|
6784
|
+
else this.excluded.delete(entry.id);
|
|
6785
|
+
}
|
|
6786
|
+
return changed;
|
|
6787
|
+
}
|
|
6692
6788
|
/** Filter a draft without changing hidden choices.
|
|
6693
6789
|
* @param query - Search input.
|
|
6694
6790
|
* @param filter - Requested loading selection.
|
|
6695
6791
|
* @returns Catalog entries in their original stable order.
|
|
6696
6792
|
*/
|
|
6697
6793
|
matchingEntries(query, filter) {
|
|
6698
|
-
return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
|
|
6794
|
+
return this.entries.map((entry) => this.entriesById.get(entry.id)).filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
|
|
6699
6795
|
}
|
|
6700
6796
|
/** Find invalid old exclusions by name or ID, independently from the protected catalog row.
|
|
6701
6797
|
* @param query - Search input.
|
|
@@ -6711,6 +6807,12 @@ window.__ModuleLoader__.load({
|
|
|
6711
6807
|
/** Device-local module selection and the computer's defaults share a read-only catalog. */
|
|
6712
6808
|
const CLIENT_MODULE_COPY = {
|
|
6713
6809
|
zh: {
|
|
6810
|
+
measure: "测量体积",
|
|
6811
|
+
measuring: "正在测量…",
|
|
6812
|
+
cancelMeasure: "取消测量",
|
|
6813
|
+
measured: "测量结束,未测到的项目保持未知。你的模块选择未变。",
|
|
6814
|
+
measureFailed: "测量未完成,已保留模块选择和已有体积。",
|
|
6815
|
+
totalPartial: "已测合计约 {size};另有 {count} 项未知",
|
|
6714
6816
|
title: "移动页面模块",
|
|
6715
6817
|
computer: "设置此电脑的移动访问默认项;电脑插件不变。",
|
|
6716
6818
|
device: "仅本设备,下次打开生效;电脑插件不变。",
|
|
@@ -6754,6 +6856,11 @@ window.__ModuleLoader__.load({
|
|
|
6754
6856
|
changes: "未保存:{count} 项改动",
|
|
6755
6857
|
unchanged: "没有未保存的改动",
|
|
6756
6858
|
results: "匹配 {visible} / {total} 项",
|
|
6859
|
+
sizeHint: "按传输体积从大到小排列;数字为 gzip 后的字节。说明取自插件包自身的描述字段,不代表上游认证。",
|
|
6860
|
+
total: "勾选合计约 {size}",
|
|
6861
|
+
totalUnknown: "勾选合计:部分模块未测到体积",
|
|
6862
|
+
minimal: "仅必需(最小加载)",
|
|
6863
|
+
minimalConfirm: "把除了启动必需项及其依赖之外全部设为不加载?保存后下次打开生效。",
|
|
6757
6864
|
retry: "重新读取",
|
|
6758
6865
|
saving: "正在保存…",
|
|
6759
6866
|
readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
|
|
@@ -6766,6 +6873,12 @@ window.__ModuleLoader__.load({
|
|
|
6766
6873
|
clearInvalid: "移除旧排除"
|
|
6767
6874
|
},
|
|
6768
6875
|
en: {
|
|
6876
|
+
measure: "Measure sizes",
|
|
6877
|
+
measuring: "Measuring…",
|
|
6878
|
+
cancelMeasure: "Cancel measurement",
|
|
6879
|
+
measured: "Measurement ended; unavailable sizes stay unknown. Your module choices are unchanged.",
|
|
6880
|
+
measureFailed: "Measurement did not complete. Your choices and known sizes are kept.",
|
|
6881
|
+
totalPartial: "Measured ≈ {size}; {count} sizes unknown",
|
|
6769
6882
|
title: "Mobile page modules",
|
|
6770
6883
|
computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
|
|
6771
6884
|
device: "For this device’s next open only. Computer plugins are unchanged.",
|
|
@@ -6809,6 +6922,11 @@ window.__ModuleLoader__.load({
|
|
|
6809
6922
|
changes: "Unsaved: {count} changes",
|
|
6810
6923
|
unchanged: "No unsaved changes",
|
|
6811
6924
|
results: "Matching {visible} / {total}",
|
|
6925
|
+
sizeHint: "Sorted by transfer size; numbers are gzipped bytes. Descriptions come from each plugin package; this is not an upstream endorsement.",
|
|
6926
|
+
total: "Selection ≈ {size}",
|
|
6927
|
+
totalUnknown: "Selection total: some sizes are unknown",
|
|
6928
|
+
minimal: "Required only (minimal load)",
|
|
6929
|
+
minimalConfirm: "Mark everything except required modules and their dependencies as not loaded? Applies on the next open after saving.",
|
|
6812
6930
|
retry: "Reread settings",
|
|
6813
6931
|
saving: "Saving…",
|
|
6814
6932
|
readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
|
|
@@ -6821,6 +6939,12 @@ window.__ModuleLoader__.load({
|
|
|
6821
6939
|
clearInvalid: "Remove old exclusion"
|
|
6822
6940
|
},
|
|
6823
6941
|
it: {
|
|
6942
|
+
measure: "Misura dimensioni",
|
|
6943
|
+
measuring: "Misurazione…",
|
|
6944
|
+
cancelMeasure: "Annulla misurazione",
|
|
6945
|
+
measured: "Misurazione terminata; le dimensioni non disponibili restano sconosciute. Le scelte non cambiano.",
|
|
6946
|
+
measureFailed: "Misurazione incompleta. Scelte e dimensioni note conservate.",
|
|
6947
|
+
totalPartial: "Misurati ≈ {size}; {count} dimensioni sconosciute",
|
|
6824
6948
|
title: "Moduli della pagina mobile",
|
|
6825
6949
|
computer: "Predefiniti mobile del computer; i plugin non cambiano.",
|
|
6826
6950
|
device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
|
|
@@ -6864,6 +6988,11 @@ window.__ModuleLoader__.load({
|
|
|
6864
6988
|
changes: "Non salvate: {count} modifiche",
|
|
6865
6989
|
unchanged: "Nessuna modifica non salvata",
|
|
6866
6990
|
results: "Corrispondenti {visible} / {total}",
|
|
6991
|
+
sizeHint: "Ordinati per dimensione trasferita; i numeri sono byte gzip. Le descrizioni provengono dal pacchetto del plugin; non sono un’approvazione upstream.",
|
|
6992
|
+
total: "Selezione ≈ {size}",
|
|
6993
|
+
totalUnknown: "Totale selezione: alcune dimensioni sono sconosciute",
|
|
6994
|
+
minimal: "Solo obbligatori (caricamento minimo)",
|
|
6995
|
+
minimalConfirm: "Impostare come non caricati tutti i moduli tranne gli obbligatori e le loro dipendenze? Vale alla prossima apertura dopo il salvataggio.",
|
|
6867
6996
|
retry: "Rileggi impostazioni",
|
|
6868
6997
|
saving: "Salvataggio…",
|
|
6869
6998
|
readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
|
|
@@ -6895,6 +7024,18 @@ window.__ModuleLoader__.load({
|
|
|
6895
7024
|
read: 15e3,
|
|
6896
7025
|
write: 2e4
|
|
6897
7026
|
};
|
|
7027
|
+
/** Describe measured bytes without presenting unmeasured modules as a complete total. */
|
|
7028
|
+
function clientModuleTotalLabel(draft, locale) {
|
|
7029
|
+
const copy = CLIENT_MODULE_COPY[locale];
|
|
7030
|
+
const unknownSizes = draft?.loadedUnknownSizeCount() ?? 0;
|
|
7031
|
+
const size = formatClientModuleSize({
|
|
7032
|
+
id: "",
|
|
7033
|
+
required: false,
|
|
7034
|
+
dependencies: [],
|
|
7035
|
+
gzipBytes: draft?.loadedTransferBytes() ?? 0
|
|
7036
|
+
}) ?? "0 KB";
|
|
7037
|
+
return unknownSizes > 0 ? copy.totalPartial.replace("{size}", size).replace("{count}", String(unknownSizes)) : copy.total.replace("{size}", size);
|
|
7038
|
+
}
|
|
6898
7039
|
/** Validate catalog response fields before creating interactive controls.
|
|
6899
7040
|
* @param value - Gateway or computer-admin JSON response.
|
|
6900
7041
|
* @returns The fields used by the module-selection dialog.
|
|
@@ -6908,10 +7049,23 @@ window.__ModuleLoader__.load({
|
|
|
6908
7049
|
return dependency;
|
|
6909
7050
|
});
|
|
6910
7051
|
if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
|
|
7052
|
+
const sizes = {};
|
|
7053
|
+
for (const field of ["bytes", "gzipBytes"]) {
|
|
7054
|
+
if (!(field in entry)) continue;
|
|
7055
|
+
const measured = entry[field];
|
|
7056
|
+
if (typeof measured !== "number" || !Number.isSafeInteger(measured) || measured < 0) throw new Error("invalid_module_catalog");
|
|
7057
|
+
sizes[field] = measured;
|
|
7058
|
+
}
|
|
7059
|
+
if ("description" in entry) {
|
|
7060
|
+
const description = entry.description;
|
|
7061
|
+
if (typeof description !== "string" || description.trim() === "" || description.length > 240) throw new Error("invalid_module_catalog");
|
|
7062
|
+
sizes.description = description;
|
|
7063
|
+
}
|
|
6911
7064
|
return {
|
|
6912
7065
|
id: entry.id,
|
|
6913
7066
|
required: entry.required,
|
|
6914
|
-
dependencies
|
|
7067
|
+
dependencies,
|
|
7068
|
+
...sizes
|
|
6915
7069
|
};
|
|
6916
7070
|
});
|
|
6917
7071
|
if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
|
|
@@ -6942,10 +7096,11 @@ window.__ModuleLoader__.load({
|
|
|
6942
7096
|
rejectCancellation(new DOMException("Dialog closed", "AbortError"));
|
|
6943
7097
|
};
|
|
6944
7098
|
signal.addEventListener("abort", abort, { once: true });
|
|
7099
|
+
const mutation = body !== void 0 && body.measure !== true;
|
|
6945
7100
|
const timer = setTimeout(() => {
|
|
6946
|
-
rejectCancellation(new ClientModuleRequestTimeout(
|
|
7101
|
+
rejectCancellation(new ClientModuleRequestTimeout(mutation));
|
|
6947
7102
|
controller.abort();
|
|
6948
|
-
},
|
|
7103
|
+
}, mutation ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.write : CLIENT_MODULE_REQUEST_TIMEOUT_MS.read);
|
|
6949
7104
|
const operation = async () => {
|
|
6950
7105
|
const init = {
|
|
6951
7106
|
method: body === void 0 ? "GET" : "POST",
|
|
@@ -6989,6 +7144,8 @@ window.__ModuleLoader__.load({
|
|
|
6989
7144
|
.dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
|
|
6990
7145
|
.dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
6991
7146
|
.dsh-module-header h2{flex:1;min-width:0;font-size:20px;line-height:1.4;margin:0;overflow-wrap:anywhere}.dsh-module-header .dsh-module-button{flex:none}
|
|
7147
|
+
.dsh-module-packageDescription{margin:2px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
7148
|
+
.dsh-module-description-full{margin:4px 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45}
|
|
6992
7149
|
.dsh-module-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding:16px;scroll-padding:12px}.dsh-module-dialog p{margin:0 0 12px;overflow-wrap:anywhere}
|
|
6993
7150
|
.dsh-module-hint,.dsh-module-source,.dsh-module-choice small,.dsh-module-groupHint,.dsh-module-results{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
6994
7151
|
.dsh-module-tools{display:grid;grid-template-columns:minmax(0,1fr) minmax(100px,.45fr);gap:12px;margin:16px 0 8px}.dsh-module-field{display:flex;flex-direction:column;gap:4px;min-width:0;font-size:13px}
|
|
@@ -7006,7 +7163,7 @@ window.__ModuleLoader__.load({
|
|
|
7006
7163
|
.dsh-module-footer p{font-size:13px;line-height:20px;margin:0 0 8px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}.dsh-module-actions .dsh-module-button{flex:1;min-width:0;white-space:normal;line-height:20px;padding:8px 12px}
|
|
7007
7164
|
.dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
|
|
7008
7165
|
.dsh-module-notice:empty{display:none}.dsh-module-notice{max-height:min(96px,24dvh);overflow:auto;font-size:14px;line-height:22px;overscroll-behavior:contain}.dsh-module-empty{padding:16px 0}
|
|
7009
|
-
@media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}}
|
|
7166
|
+
@media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}.dsh-module-header{flex-wrap:wrap;gap:8px}.dsh-module-header h2{flex:1 1 100%}.dsh-module-header .dsh-module-button{flex:1 1 0;min-width:0;white-space:normal;overflow-wrap:anywhere;line-height:1.4;padding:8px 12px}}
|
|
7010
7167
|
.dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
|
|
7011
7168
|
`;
|
|
7012
7169
|
const openDialogs = /* @__PURE__ */ new Set();
|
|
@@ -7053,7 +7210,9 @@ window.__ModuleLoader__.load({
|
|
|
7053
7210
|
const title = document.createElement("h2");
|
|
7054
7211
|
title.textContent = copy.title;
|
|
7055
7212
|
const close = button(copy.close);
|
|
7056
|
-
|
|
7213
|
+
const measure = button(copy.measure);
|
|
7214
|
+
measure.dataset.clientModuleMeasure = "true";
|
|
7215
|
+
header.append(title, measure, close);
|
|
7057
7216
|
const body = document.createElement("div");
|
|
7058
7217
|
body.className = "dsh-module-body";
|
|
7059
7218
|
const intro = paragraph(copy[scope]);
|
|
@@ -7063,6 +7222,7 @@ window.__ModuleLoader__.load({
|
|
|
7063
7222
|
const helpTitle = document.createElement("summary");
|
|
7064
7223
|
helpTitle.textContent = copy.help;
|
|
7065
7224
|
help.append(helpTitle, hint);
|
|
7225
|
+
help.append(paragraph(copy.sizeHint, "dsh-module-hint"));
|
|
7066
7226
|
const source = paragraph("", "dsh-module-source");
|
|
7067
7227
|
const status = paragraph("", "dsh-module-notice");
|
|
7068
7228
|
status.setAttribute("role", "status");
|
|
@@ -7109,14 +7269,16 @@ window.__ModuleLoader__.load({
|
|
|
7109
7269
|
actions.className = "dsh-module-actions";
|
|
7110
7270
|
const save = button(copy.save);
|
|
7111
7271
|
save.classList.add("dsh-module-save");
|
|
7272
|
+
const minimal = button(copy.minimal);
|
|
7112
7273
|
const reset = button(copy.reset);
|
|
7113
7274
|
const reread = button(copy.retry);
|
|
7114
7275
|
actions.append(save, reset, reread);
|
|
7115
7276
|
footer.append(changes, status, actions);
|
|
7116
|
-
body.append(intro, source, tools, results, list, help);
|
|
7277
|
+
body.append(intro, source, tools, results, minimal, list, help);
|
|
7117
7278
|
dialog.append(header, body, footer);
|
|
7118
7279
|
let disposed = false;
|
|
7119
7280
|
let busy;
|
|
7281
|
+
let measurementController;
|
|
7120
7282
|
let generation = 0;
|
|
7121
7283
|
let draft;
|
|
7122
7284
|
let unconfirmedWrite = false;
|
|
@@ -7132,11 +7294,15 @@ window.__ModuleLoader__.load({
|
|
|
7132
7294
|
const changed = draft?.changeCount ?? 0;
|
|
7133
7295
|
save.disabled = busy !== void 0 || changed === 0;
|
|
7134
7296
|
reset.disabled = busy !== void 0 || draft === void 0;
|
|
7297
|
+
minimal.disabled = busy !== void 0 || draft === void 0;
|
|
7298
|
+
measure.disabled = busy !== void 0 && busy !== "measure" || draft === void 0;
|
|
7299
|
+
measure.textContent = busy === "measure" ? copy.cancelMeasure : copy.measure;
|
|
7135
7300
|
reread.disabled = busy !== void 0;
|
|
7136
7301
|
search.disabled = busy !== void 0 || draft === void 0;
|
|
7137
7302
|
filter.disabled = search.disabled;
|
|
7138
7303
|
save.textContent = busy === "write" ? copy.saving : copy.save;
|
|
7139
|
-
|
|
7304
|
+
const total = clientModuleTotalLabel(draft, locale);
|
|
7305
|
+
changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${total} · ${copy.nextOpen}`;
|
|
7140
7306
|
list.setAttribute("aria-busy", String(busy !== void 0));
|
|
7141
7307
|
for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
|
|
7142
7308
|
for (const action of invalidActions.values()) action.disabled = busy !== void 0;
|
|
@@ -7200,7 +7366,7 @@ window.__ModuleLoader__.load({
|
|
|
7200
7366
|
"advanced",
|
|
7201
7367
|
"required"
|
|
7202
7368
|
]) {
|
|
7203
|
-
const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
|
|
7369
|
+
const entries = sortClientModulesBySize(visible.filter((entry) => clientModuleGroup(entry) === group));
|
|
7204
7370
|
if (entries.length === 0) continue;
|
|
7205
7371
|
const section = document.createElement(group === "extensions" ? "section" : "details");
|
|
7206
7372
|
section.className = "dsh-module-group";
|
|
@@ -7238,9 +7404,19 @@ window.__ModuleLoader__.load({
|
|
|
7238
7404
|
shortName.textContent = clientModuleDisplayName(entry.id);
|
|
7239
7405
|
const selected = document.createElement("small");
|
|
7240
7406
|
selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
|
|
7241
|
-
|
|
7407
|
+
const size = formatClientModuleSize(entry);
|
|
7408
|
+
const sizeLabel = document.createElement("small");
|
|
7409
|
+
sizeLabel.className = "dsh-module-size";
|
|
7410
|
+
sizeLabel.textContent = size ?? "";
|
|
7411
|
+
if (size !== void 0) sizeLabel.dataset.clientModuleSize = "true";
|
|
7412
|
+
name.append(shortName, selected, sizeLabel);
|
|
7242
7413
|
label.append(input, name);
|
|
7243
7414
|
row.append(label);
|
|
7415
|
+
if (entry.description !== void 0) {
|
|
7416
|
+
const summary = paragraph(entry.description, "dsh-module-packageDescription");
|
|
7417
|
+
summary.dataset.clientModuleDescription = "true";
|
|
7418
|
+
row.append(summary);
|
|
7419
|
+
}
|
|
7244
7420
|
const detail = document.createElement("details");
|
|
7245
7421
|
detail.className = "dsh-module-entryDetails";
|
|
7246
7422
|
const detailSummary = document.createElement("summary");
|
|
@@ -7249,7 +7425,9 @@ window.__ModuleLoader__.load({
|
|
|
7249
7425
|
const code = document.createElement("code");
|
|
7250
7426
|
code.textContent = entry.id;
|
|
7251
7427
|
id.append(code);
|
|
7252
|
-
detail.append(detailSummary, id
|
|
7428
|
+
detail.append(detailSummary, id);
|
|
7429
|
+
if (entry.description !== void 0) detail.append(paragraph(entry.description, "dsh-module-description-full"));
|
|
7430
|
+
detail.append(paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
|
|
7253
7431
|
row.append(detail);
|
|
7254
7432
|
input.addEventListener("change", () => {
|
|
7255
7433
|
if (busy !== void 0 || draft === void 0) return;
|
|
@@ -7284,23 +7462,36 @@ window.__ModuleLoader__.load({
|
|
|
7284
7462
|
};
|
|
7285
7463
|
const runRequest = (payload) => {
|
|
7286
7464
|
if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
|
|
7465
|
+
const measuring = payload?.measure === true;
|
|
7466
|
+
const mutation = payload !== void 0 && !measuring;
|
|
7287
7467
|
const ownGeneration = ++generation;
|
|
7288
|
-
busy =
|
|
7289
|
-
if (
|
|
7468
|
+
busy = measuring ? "measure" : mutation ? "write" : "read";
|
|
7469
|
+
if (mutation) unconfirmedWrite = true;
|
|
7470
|
+
if (measuring) measurementController = new AbortController();
|
|
7290
7471
|
status.classList.remove("dsh-module-error");
|
|
7291
|
-
status.textContent =
|
|
7472
|
+
status.textContent = measuring ? copy.measuring : mutation ? copy.saving : copy.loading;
|
|
7292
7473
|
updateControls();
|
|
7293
|
-
requestClientModuleView(scope, controller.signal, payload).then((view) => {
|
|
7474
|
+
requestClientModuleView(scope, measuring ? measurementController.signal : controller.signal, payload).then((view) => {
|
|
7294
7475
|
if (disposed || ownGeneration !== generation) return;
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7476
|
+
if (measuring && draft !== void 0) {
|
|
7477
|
+
draft.updateMeasurements(view.entries);
|
|
7478
|
+
renderList();
|
|
7479
|
+
} else {
|
|
7480
|
+
unconfirmedWrite = false;
|
|
7481
|
+
render(view);
|
|
7482
|
+
}
|
|
7483
|
+
status.textContent = measuring ? copy.measured : mutation ? copy.saved : "";
|
|
7298
7484
|
}, (error) => {
|
|
7299
7485
|
if (disposed || ownGeneration !== generation) return;
|
|
7486
|
+
if (measuring) {
|
|
7487
|
+
status.textContent = copy.measureFailed;
|
|
7488
|
+
return;
|
|
7489
|
+
}
|
|
7300
7490
|
if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
|
|
7301
|
-
failed(error,
|
|
7491
|
+
failed(error, mutation);
|
|
7302
7492
|
}).finally(() => {
|
|
7303
7493
|
if (disposed || ownGeneration !== generation) return;
|
|
7494
|
+
measurementController = void 0;
|
|
7304
7495
|
busy = void 0;
|
|
7305
7496
|
updateControls();
|
|
7306
7497
|
});
|
|
@@ -7313,6 +7504,18 @@ window.__ModuleLoader__.load({
|
|
|
7313
7504
|
reset.addEventListener("click", () => {
|
|
7314
7505
|
if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
|
|
7315
7506
|
});
|
|
7507
|
+
minimal.addEventListener("click", () => {
|
|
7508
|
+
if (busy !== void 0 || draft === void 0 || !window.confirm(copy.minimalConfirm)) return;
|
|
7509
|
+
if (draft.applyMinimalSelection()) {
|
|
7510
|
+
renderList();
|
|
7511
|
+
search.focus({ preventScroll: true });
|
|
7512
|
+
}
|
|
7513
|
+
updateControls();
|
|
7514
|
+
});
|
|
7515
|
+
measure.addEventListener("click", () => {
|
|
7516
|
+
if (busy === "measure") measurementController?.abort();
|
|
7517
|
+
else if (busy === void 0 && draft !== void 0) runRequest({ measure: true });
|
|
7518
|
+
});
|
|
7316
7519
|
reread.addEventListener("click", () => {
|
|
7317
7520
|
if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
|
|
7318
7521
|
});
|
|
@@ -7321,6 +7524,7 @@ window.__ModuleLoader__.load({
|
|
|
7321
7524
|
disposed = true;
|
|
7322
7525
|
++generation;
|
|
7323
7526
|
controller.abort();
|
|
7527
|
+
measurementController?.abort();
|
|
7324
7528
|
openDialogs.delete(openDialog);
|
|
7325
7529
|
window.removeEventListener("dsh-mobile:native-back", onBack, true);
|
|
7326
7530
|
window.removeEventListener("keydown", onEscape, true);
|
|
@@ -7890,6 +8094,1074 @@ window.__ModuleLoader__.load({
|
|
|
7890
8094
|
};
|
|
7891
8095
|
}
|
|
7892
8096
|
//#endregion
|
|
8097
|
+
//#region src/mobile-directory-browser.ts
|
|
8098
|
+
/** Expected Gateway or response-validation failure for one read-only listing. */
|
|
8099
|
+
var MobileDirectoryReadError = class extends Error {
|
|
8100
|
+
code;
|
|
8101
|
+
constructor(code, options) {
|
|
8102
|
+
super(`Mobile directory listing failed: ${code}`, options);
|
|
8103
|
+
this.code = code;
|
|
8104
|
+
this.name = "MobileDirectoryReadError";
|
|
8105
|
+
}
|
|
8106
|
+
};
|
|
8107
|
+
/** Accept portable absolute spellings; the Host remains authoritative for its platform. */
|
|
8108
|
+
function isMobileDirectoryPath(path) {
|
|
8109
|
+
return path.length > 0 && path.length <= 8192 && !path.includes("\0") && (path.startsWith("/") || /^[a-z]:[\\/]/iu.test(path) || /^\\\\[^\\/]+[\\/][^\\/]+/u.test(path));
|
|
8110
|
+
}
|
|
8111
|
+
function object(value) {
|
|
8112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
8113
|
+
}
|
|
8114
|
+
/** Validate a wire response before publishing names or adopting a Host path. */
|
|
8115
|
+
function parseMobileDirectoryListing(value) {
|
|
8116
|
+
const record = object(value);
|
|
8117
|
+
const fail = () => {
|
|
8118
|
+
throw new MobileDirectoryReadError("invalid-response");
|
|
8119
|
+
};
|
|
8120
|
+
if (record === void 0 || typeof record.path !== "string" || !isMobileDirectoryPath(record.path) || typeof record.truncated !== "boolean" || !Array.isArray(record.entries) || record.entries.length > 500) return fail();
|
|
8121
|
+
const parent = record.parent;
|
|
8122
|
+
if (parent !== void 0 && (typeof parent !== "string" || !isMobileDirectoryPath(parent))) return fail();
|
|
8123
|
+
const paths = /* @__PURE__ */ new Set();
|
|
8124
|
+
const entries = [];
|
|
8125
|
+
for (const candidate of record.entries) {
|
|
8126
|
+
const entry = object(candidate);
|
|
8127
|
+
if (entry === void 0 || typeof entry.name !== "string" || entry.name.length === 0 || entry.name.includes("\0") || typeof entry.path !== "string" || !isMobileDirectoryPath(entry.path) || paths.has(entry.path)) return fail();
|
|
8128
|
+
paths.add(entry.path);
|
|
8129
|
+
entries.push(Object.freeze({
|
|
8130
|
+
name: entry.name,
|
|
8131
|
+
path: entry.path
|
|
8132
|
+
}));
|
|
8133
|
+
}
|
|
8134
|
+
return Object.freeze({
|
|
8135
|
+
path: record.path,
|
|
8136
|
+
...parent === void 0 ? {} : { parent },
|
|
8137
|
+
entries: Object.freeze(entries),
|
|
8138
|
+
truncated: record.truncated
|
|
8139
|
+
});
|
|
8140
|
+
}
|
|
8141
|
+
const CLOSED = Object.freeze({
|
|
8142
|
+
open: false,
|
|
8143
|
+
phase: "idle",
|
|
8144
|
+
listing: null,
|
|
8145
|
+
draft: "",
|
|
8146
|
+
error: null,
|
|
8147
|
+
hostBusy: false,
|
|
8148
|
+
committed: false,
|
|
8149
|
+
creating: false
|
|
8150
|
+
});
|
|
8151
|
+
/** Read navigation stops on close; committing is final until the owner withdraws this flow. */
|
|
8152
|
+
var MobileDirectoryBrowser = class {
|
|
8153
|
+
read;
|
|
8154
|
+
deadlineMs;
|
|
8155
|
+
snapshot = CLOSED;
|
|
8156
|
+
listeners = /* @__PURE__ */ new Set();
|
|
8157
|
+
tasks = /* @__PURE__ */ new Set();
|
|
8158
|
+
taskFailures = [];
|
|
8159
|
+
activeRead;
|
|
8160
|
+
generation = 0;
|
|
8161
|
+
disposed = false;
|
|
8162
|
+
lastTarget = null;
|
|
8163
|
+
constructor(read, deadlineMs = 3e4) {
|
|
8164
|
+
this.read = read;
|
|
8165
|
+
this.deadlineMs = deadlineMs;
|
|
8166
|
+
}
|
|
8167
|
+
getSnapshot = () => this.snapshot;
|
|
8168
|
+
/** Subscribe to this occurrence; the returned disposer removes the observer. */
|
|
8169
|
+
subscribe(listener) {
|
|
8170
|
+
this.listeners.add(listener);
|
|
8171
|
+
return () => {
|
|
8172
|
+
this.listeners.delete(listener);
|
|
8173
|
+
};
|
|
8174
|
+
}
|
|
8175
|
+
/** Start a new open edge at the Host home; returns false after permanent disposal. */
|
|
8176
|
+
start() {
|
|
8177
|
+
if (this.disposed || this.snapshot.open) return false;
|
|
8178
|
+
this.publish({
|
|
8179
|
+
...CLOSED,
|
|
8180
|
+
open: true
|
|
8181
|
+
});
|
|
8182
|
+
return this.navigate(null);
|
|
8183
|
+
}
|
|
8184
|
+
/** Editing invalidates any pending scan and cannot commit the previously listed path. */
|
|
8185
|
+
setDraft(draft) {
|
|
8186
|
+
if (!this.canNavigate() || draft === this.snapshot.draft) return;
|
|
8187
|
+
this.withdrawRead();
|
|
8188
|
+
this.publish({
|
|
8189
|
+
...this.snapshot,
|
|
8190
|
+
draft,
|
|
8191
|
+
phase: "idle",
|
|
8192
|
+
error: isMobileDirectoryPath(draft) ? null : "bad-path"
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8195
|
+
/** Project the owner's adoption state without undoing this flow's synchronous commit claim. */
|
|
8196
|
+
setHostBusy(hostBusy) {
|
|
8197
|
+
if (this.disposed || this.snapshot.hostBusy === hostBusy) return;
|
|
8198
|
+
this.publish({
|
|
8199
|
+
...this.snapshot,
|
|
8200
|
+
hostBusy
|
|
8201
|
+
});
|
|
8202
|
+
}
|
|
8203
|
+
/** Block navigation/adoption while this flow's child-folder editor owns interaction. */
|
|
8204
|
+
setCreating(creating) {
|
|
8205
|
+
if (this.disposed || this.snapshot.creating === creating) return;
|
|
8206
|
+
this.publish({
|
|
8207
|
+
...this.snapshot,
|
|
8208
|
+
creating
|
|
8209
|
+
});
|
|
8210
|
+
}
|
|
8211
|
+
/** Read a folder or the initial home; superseded results cannot publish into the new request. */
|
|
8212
|
+
navigate(path) {
|
|
8213
|
+
if (!this.canNavigate()) return false;
|
|
8214
|
+
this.withdrawRead();
|
|
8215
|
+
if (path !== null && !isMobileDirectoryPath(path)) {
|
|
8216
|
+
this.publish({
|
|
8217
|
+
...this.snapshot,
|
|
8218
|
+
draft: path,
|
|
8219
|
+
phase: "error",
|
|
8220
|
+
error: "bad-path"
|
|
8221
|
+
});
|
|
8222
|
+
return false;
|
|
8223
|
+
}
|
|
8224
|
+
this.lastTarget = path;
|
|
8225
|
+
const controller = new AbortController();
|
|
8226
|
+
this.activeRead = controller;
|
|
8227
|
+
const generation = this.generation;
|
|
8228
|
+
this.publish({
|
|
8229
|
+
...this.snapshot,
|
|
8230
|
+
phase: "loading",
|
|
8231
|
+
error: null,
|
|
8232
|
+
...path === null ? {} : { draft: path }
|
|
8233
|
+
});
|
|
8234
|
+
const task = (async () => {
|
|
8235
|
+
const timeout = setTimeout(() => {
|
|
8236
|
+
controller.abort(new MobileDirectoryReadError("timeout"));
|
|
8237
|
+
}, this.deadlineMs);
|
|
8238
|
+
try {
|
|
8239
|
+
const value = await this.read(path, controller.signal);
|
|
8240
|
+
controller.signal.throwIfAborted();
|
|
8241
|
+
const listing = parseMobileDirectoryListing(value);
|
|
8242
|
+
if (!this.current(generation)) return;
|
|
8243
|
+
this.publish({
|
|
8244
|
+
...this.snapshot,
|
|
8245
|
+
phase: "idle",
|
|
8246
|
+
listing,
|
|
8247
|
+
draft: listing.path,
|
|
8248
|
+
error: null
|
|
8249
|
+
});
|
|
8250
|
+
} catch (error) {
|
|
8251
|
+
if (!this.current(generation)) return;
|
|
8252
|
+
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
8253
|
+
this.publish({
|
|
8254
|
+
...this.snapshot,
|
|
8255
|
+
phase: "error",
|
|
8256
|
+
error: reason instanceof MobileDirectoryReadError ? reason.code : "request-failed"
|
|
8257
|
+
});
|
|
8258
|
+
} finally {
|
|
8259
|
+
clearTimeout(timeout);
|
|
8260
|
+
if (this.activeRead === controller) this.activeRead = void 0;
|
|
8261
|
+
}
|
|
8262
|
+
})();
|
|
8263
|
+
this.tasks.add(task);
|
|
8264
|
+
task.then(() => {
|
|
8265
|
+
this.tasks.delete(task);
|
|
8266
|
+
}, (error) => {
|
|
8267
|
+
this.tasks.delete(task);
|
|
8268
|
+
this.taskFailures.push(error);
|
|
8269
|
+
});
|
|
8270
|
+
return true;
|
|
8271
|
+
}
|
|
8272
|
+
/** Retry only the explicitly failed read, without adopting or replaying a write. */
|
|
8273
|
+
retry() {
|
|
8274
|
+
return this.navigate(this.lastTarget);
|
|
8275
|
+
}
|
|
8276
|
+
/** Whether the confirmed listing still matches the editable path and can be adopted. */
|
|
8277
|
+
canConfirm() {
|
|
8278
|
+
const value = this.snapshot;
|
|
8279
|
+
return this.canNavigate() && value.phase === "idle" && value.error === null && value.listing !== null && value.draft === value.listing.path;
|
|
8280
|
+
}
|
|
8281
|
+
/** Claim one adoption synchronously; returns no path for stale input, busy state or duplicate taps. */
|
|
8282
|
+
confirm() {
|
|
8283
|
+
if (!this.canConfirm()) return void 0;
|
|
8284
|
+
const path = this.snapshot.listing.path;
|
|
8285
|
+
this.publish({
|
|
8286
|
+
...this.snapshot,
|
|
8287
|
+
committed: true
|
|
8288
|
+
});
|
|
8289
|
+
return path;
|
|
8290
|
+
}
|
|
8291
|
+
/** Cancel reading immediately; a committed/Host-busy adoption cannot be reported as cancelled. */
|
|
8292
|
+
cancel() {
|
|
8293
|
+
if (!this.canClose()) return false;
|
|
8294
|
+
this.withdrawRead();
|
|
8295
|
+
this.publish(CLOSED);
|
|
8296
|
+
return true;
|
|
8297
|
+
}
|
|
8298
|
+
/** Withdraw callbacks immediately and await every admitted read before disposal completes. */
|
|
8299
|
+
async dispose() {
|
|
8300
|
+
this.disposed = true;
|
|
8301
|
+
this.withdrawRead();
|
|
8302
|
+
this.snapshot = CLOSED;
|
|
8303
|
+
this.listeners.clear();
|
|
8304
|
+
await Promise.allSettled([...this.tasks]);
|
|
8305
|
+
if (this.taskFailures.length > 0) throw new AggregateError(this.taskFailures, "Mobile directory observers failed during a listing");
|
|
8306
|
+
}
|
|
8307
|
+
canClose() {
|
|
8308
|
+
return !this.disposed && this.snapshot.open && !this.snapshot.hostBusy && !this.snapshot.committed;
|
|
8309
|
+
}
|
|
8310
|
+
canNavigate() {
|
|
8311
|
+
return this.canClose() && !this.snapshot.creating;
|
|
8312
|
+
}
|
|
8313
|
+
current(generation) {
|
|
8314
|
+
return !this.disposed && this.snapshot.open && generation === this.generation;
|
|
8315
|
+
}
|
|
8316
|
+
withdrawRead() {
|
|
8317
|
+
this.generation++;
|
|
8318
|
+
this.activeRead?.abort();
|
|
8319
|
+
this.activeRead = void 0;
|
|
8320
|
+
}
|
|
8321
|
+
publish(snapshot) {
|
|
8322
|
+
this.snapshot = Object.freeze(snapshot);
|
|
8323
|
+
for (const listener of this.listeners) listener();
|
|
8324
|
+
}
|
|
8325
|
+
};
|
|
8326
|
+
/** A creation failure without raw response bodies or filesystem details. */
|
|
8327
|
+
var MobileDirectoryCreateError = class extends Error {
|
|
8328
|
+
code;
|
|
8329
|
+
constructor(code) {
|
|
8330
|
+
super(`Mobile directory creation failed: ${code}`);
|
|
8331
|
+
this.code = code;
|
|
8332
|
+
this.name = "MobileDirectoryCreateError";
|
|
8333
|
+
}
|
|
8334
|
+
};
|
|
8335
|
+
/** Portable single-segment validation; the Host additionally enforces its filesystem rules. */
|
|
8336
|
+
function isMobileDirectoryName(name) {
|
|
8337
|
+
return name.trim() !== "" && name.length <= 255 && name !== "." && name !== ".." && !/[\\/\0]/u.test(name);
|
|
8338
|
+
}
|
|
8339
|
+
const CREATE_CLOSED = Object.freeze({
|
|
8340
|
+
open: false,
|
|
8341
|
+
parent: null,
|
|
8342
|
+
name: "",
|
|
8343
|
+
pending: false,
|
|
8344
|
+
error: null
|
|
8345
|
+
});
|
|
8346
|
+
/** Owns one non-replayed write, its deadline, cancellation and physical teardown. */
|
|
8347
|
+
var MobileDirectoryCreator = class {
|
|
8348
|
+
create;
|
|
8349
|
+
onCreated;
|
|
8350
|
+
deadlineMs;
|
|
8351
|
+
snapshot = CREATE_CLOSED;
|
|
8352
|
+
listeners = /* @__PURE__ */ new Set();
|
|
8353
|
+
tasks = /* @__PURE__ */ new Set();
|
|
8354
|
+
failures = [];
|
|
8355
|
+
active;
|
|
8356
|
+
generation = 0;
|
|
8357
|
+
disposed = false;
|
|
8358
|
+
constructor(create, onCreated, deadlineMs = 3e4) {
|
|
8359
|
+
this.create = create;
|
|
8360
|
+
this.onCreated = onCreated;
|
|
8361
|
+
this.deadlineMs = deadlineMs;
|
|
8362
|
+
}
|
|
8363
|
+
getSnapshot = () => this.snapshot;
|
|
8364
|
+
/** Subscribe to this editor; the returned disposer withdraws the observer. */
|
|
8365
|
+
subscribe(listener) {
|
|
8366
|
+
this.listeners.add(listener);
|
|
8367
|
+
return () => {
|
|
8368
|
+
this.listeners.delete(listener);
|
|
8369
|
+
};
|
|
8370
|
+
}
|
|
8371
|
+
/** Open against one already confirmed parent; cancelled physical work retains admission. */
|
|
8372
|
+
open(parent) {
|
|
8373
|
+
if (this.disposed || this.snapshot.open || this.tasks.size > 0 || !isMobileDirectoryPath(parent)) return false;
|
|
8374
|
+
this.publish({
|
|
8375
|
+
...CREATE_CLOSED,
|
|
8376
|
+
open: true,
|
|
8377
|
+
parent
|
|
8378
|
+
});
|
|
8379
|
+
return true;
|
|
8380
|
+
}
|
|
8381
|
+
/** Edit only while no write is in flight; typing never starts a filesystem operation. */
|
|
8382
|
+
setName(name) {
|
|
8383
|
+
if (this.disposed || !this.snapshot.open || this.snapshot.pending) return;
|
|
8384
|
+
this.publish({
|
|
8385
|
+
...this.snapshot,
|
|
8386
|
+
name,
|
|
8387
|
+
error: null
|
|
8388
|
+
});
|
|
8389
|
+
}
|
|
8390
|
+
/** Admit one explicit creation. Repeated clicks/Enter cannot replay an admitted request. */
|
|
8391
|
+
submit() {
|
|
8392
|
+
if (this.disposed || !this.snapshot.open || this.snapshot.pending || this.tasks.size > 0 || this.snapshot.parent === null) return false;
|
|
8393
|
+
const name = this.snapshot.name.trim();
|
|
8394
|
+
if (!isMobileDirectoryName(name)) {
|
|
8395
|
+
this.publish({
|
|
8396
|
+
...this.snapshot,
|
|
8397
|
+
error: "invalid-name"
|
|
8398
|
+
});
|
|
8399
|
+
return false;
|
|
8400
|
+
}
|
|
8401
|
+
const parent = this.snapshot.parent;
|
|
8402
|
+
const generation = this.generation;
|
|
8403
|
+
const controller = new AbortController();
|
|
8404
|
+
this.active = controller;
|
|
8405
|
+
this.publish({
|
|
8406
|
+
...this.snapshot,
|
|
8407
|
+
pending: true,
|
|
8408
|
+
error: null
|
|
8409
|
+
});
|
|
8410
|
+
if (!this.current(generation) || controller.signal.aborted) {
|
|
8411
|
+
if (this.active === controller) this.active = void 0;
|
|
8412
|
+
if (!this.disposed) this.publish({
|
|
8413
|
+
...this.snapshot,
|
|
8414
|
+
pending: false
|
|
8415
|
+
});
|
|
8416
|
+
return false;
|
|
8417
|
+
}
|
|
8418
|
+
const timer = setTimeout(() => {
|
|
8419
|
+
controller.abort(new MobileDirectoryCreateError("timeout"));
|
|
8420
|
+
if (this.current(generation)) this.publish({
|
|
8421
|
+
...this.snapshot,
|
|
8422
|
+
error: "timeout"
|
|
8423
|
+
});
|
|
8424
|
+
}, this.deadlineMs);
|
|
8425
|
+
const task = (async () => {
|
|
8426
|
+
let created;
|
|
8427
|
+
try {
|
|
8428
|
+
created = await this.create(parent, name, controller.signal);
|
|
8429
|
+
controller.signal.throwIfAborted();
|
|
8430
|
+
if (!isMobileDirectoryPath(created)) throw new MobileDirectoryCreateError("invalid-response");
|
|
8431
|
+
} catch (error) {
|
|
8432
|
+
created = void 0;
|
|
8433
|
+
if (this.current(generation)) {
|
|
8434
|
+
const failure = controller.signal.aborted ? controller.signal.reason : error;
|
|
8435
|
+
this.publish({
|
|
8436
|
+
...this.snapshot,
|
|
8437
|
+
error: failure instanceof MobileDirectoryCreateError ? failure.code : "request-failed"
|
|
8438
|
+
});
|
|
8439
|
+
}
|
|
8440
|
+
} finally {
|
|
8441
|
+
clearTimeout(timer);
|
|
8442
|
+
if (this.active === controller) this.active = void 0;
|
|
8443
|
+
if (!this.disposed) this.publish({
|
|
8444
|
+
...this.snapshot,
|
|
8445
|
+
pending: false
|
|
8446
|
+
});
|
|
8447
|
+
}
|
|
8448
|
+
if (created !== void 0 && this.current(generation)) {
|
|
8449
|
+
this.publish(CREATE_CLOSED);
|
|
8450
|
+
this.onCreated(created);
|
|
8451
|
+
}
|
|
8452
|
+
})();
|
|
8453
|
+
this.tasks.add(task);
|
|
8454
|
+
task.then(() => {
|
|
8455
|
+
this.tasks.delete(task);
|
|
8456
|
+
}, (error) => {
|
|
8457
|
+
this.tasks.delete(task);
|
|
8458
|
+
this.failures.push(error);
|
|
8459
|
+
});
|
|
8460
|
+
return true;
|
|
8461
|
+
}
|
|
8462
|
+
/** Withdraw the editor and late results; an already completed mkdir is not rolled back. */
|
|
8463
|
+
close() {
|
|
8464
|
+
if (this.disposed || !this.snapshot.open) return false;
|
|
8465
|
+
this.generation++;
|
|
8466
|
+
this.active?.abort();
|
|
8467
|
+
this.publish({
|
|
8468
|
+
...CREATE_CLOSED,
|
|
8469
|
+
pending: this.active !== void 0
|
|
8470
|
+
});
|
|
8471
|
+
return true;
|
|
8472
|
+
}
|
|
8473
|
+
/** Stop admission synchronously and await every accepted write before completion. */
|
|
8474
|
+
async dispose() {
|
|
8475
|
+
this.disposed = true;
|
|
8476
|
+
this.generation++;
|
|
8477
|
+
this.active?.abort();
|
|
8478
|
+
this.snapshot = CREATE_CLOSED;
|
|
8479
|
+
this.listeners.clear();
|
|
8480
|
+
await Promise.allSettled([...this.tasks]);
|
|
8481
|
+
if (this.failures.length > 0) throw new AggregateError(this.failures, "Mobile directory creation observers failed");
|
|
8482
|
+
}
|
|
8483
|
+
current(generation) {
|
|
8484
|
+
return !this.disposed && this.snapshot.open && generation === this.generation;
|
|
8485
|
+
}
|
|
8486
|
+
publish(snapshot) {
|
|
8487
|
+
this.snapshot = Object.freeze(snapshot);
|
|
8488
|
+
for (const listener of this.listeners) listener();
|
|
8489
|
+
}
|
|
8490
|
+
};
|
|
8491
|
+
//#endregion
|
|
8492
|
+
//#region src/mobile-directory-picker.ts
|
|
8493
|
+
/** Dedicated Mobile directory-flow occupant; stock DSH retains its own chooser. */
|
|
8494
|
+
const MESSAGES = {
|
|
8495
|
+
zh: {
|
|
8496
|
+
title: "选择工作区目录",
|
|
8497
|
+
path: "目录路径",
|
|
8498
|
+
go: "前往",
|
|
8499
|
+
home: "主目录",
|
|
8500
|
+
up: "上一级",
|
|
8501
|
+
cancel: "取消",
|
|
8502
|
+
close: "关闭",
|
|
8503
|
+
open: "打开",
|
|
8504
|
+
retry: "重试",
|
|
8505
|
+
loading: "正在读取文件夹…",
|
|
8506
|
+
adopting: "正在打开工作区…",
|
|
8507
|
+
empty: "此目录中没有子文件夹",
|
|
8508
|
+
truncated: "文件夹过多,仅显示开头部分;可输入完整路径前往其他目录",
|
|
8509
|
+
"bad-path": "请输入电脑上的绝对目录路径,例如 C:\\projects 或 /home/user/projects",
|
|
8510
|
+
unavailable: "无法读取此目录,请检查路径和访问权限后重试",
|
|
8511
|
+
unauthorized: "连接授权不可用,请关闭窗口并重新连接电脑",
|
|
8512
|
+
"invalid-response": "目录响应不可用,请重试或重新连接电脑",
|
|
8513
|
+
"request-failed": "读取目录失败,请检查连接后重试",
|
|
8514
|
+
timeout: "读取目录超时,请重试或取消",
|
|
8515
|
+
failedOpen: "无法打开目录选择器,请重新连接后重试",
|
|
8516
|
+
create: "新建文件夹",
|
|
8517
|
+
createTitle: "新建文件夹名称",
|
|
8518
|
+
createAction: "创建",
|
|
8519
|
+
createInvalid: "请输入单个文件夹名称,不能包含路径分隔符。",
|
|
8520
|
+
createExists: "同名文件夹已存在。",
|
|
8521
|
+
createFailed: "无法创建文件夹,请检查该目录的写入权限后重试。",
|
|
8522
|
+
createParent: "创建位置",
|
|
8523
|
+
creating: "正在创建文件夹…",
|
|
8524
|
+
createStopping: "正在结束创建请求…",
|
|
8525
|
+
createUnknown: "无法确认创建结果,文件夹可能已创建。请返回并重新读取目录,确认后再决定是否重试。"
|
|
8526
|
+
},
|
|
8527
|
+
en: {
|
|
8528
|
+
title: "Select workspace directory",
|
|
8529
|
+
path: "Directory path",
|
|
8530
|
+
go: "Go",
|
|
8531
|
+
home: "Home",
|
|
8532
|
+
up: "Up one level",
|
|
8533
|
+
cancel: "Cancel",
|
|
8534
|
+
close: "Close",
|
|
8535
|
+
open: "Open",
|
|
8536
|
+
retry: "Retry",
|
|
8537
|
+
loading: "Loading folders…",
|
|
8538
|
+
adopting: "Opening workspace…",
|
|
8539
|
+
empty: "No subfolders in this directory",
|
|
8540
|
+
truncated: "Only the first folders are shown; enter a full path to open another directory",
|
|
8541
|
+
"bad-path": "Enter an absolute computer path, such as C:\\projects or /home/user/projects",
|
|
8542
|
+
unavailable: "Could not read this directory. Check its path and access permissions, then retry.",
|
|
8543
|
+
unauthorized: "Connection authorization is unavailable. Close this dialog and reconnect to the computer.",
|
|
8544
|
+
"invalid-response": "The directory response is unavailable. Retry or reconnect to the computer.",
|
|
8545
|
+
"request-failed": "Could not load this directory. Check the connection and retry.",
|
|
8546
|
+
timeout: "Loading timed out. Retry or cancel.",
|
|
8547
|
+
failedOpen: "Could not open the directory picker. Reconnect and retry.",
|
|
8548
|
+
create: "New folder",
|
|
8549
|
+
createTitle: "New folder name",
|
|
8550
|
+
createAction: "Create",
|
|
8551
|
+
createInvalid: "Enter a single folder name without path separators.",
|
|
8552
|
+
createExists: "A folder with that name already exists.",
|
|
8553
|
+
createFailed: "Could not create the folder. Check write access to this directory and retry.",
|
|
8554
|
+
createParent: "Create in",
|
|
8555
|
+
creating: "Creating folder…",
|
|
8556
|
+
createStopping: "Finishing the creation request…",
|
|
8557
|
+
createUnknown: "The creation result is unknown; the folder may already exist. Go back and reload the directory before deciding whether to retry."
|
|
8558
|
+
},
|
|
8559
|
+
it: {
|
|
8560
|
+
title: "Seleziona la cartella del workspace",
|
|
8561
|
+
path: "Percorso della cartella",
|
|
8562
|
+
go: "Vai",
|
|
8563
|
+
home: "Home",
|
|
8564
|
+
up: "Cartella superiore",
|
|
8565
|
+
cancel: "Annulla",
|
|
8566
|
+
close: "Chiudi",
|
|
8567
|
+
open: "Apri",
|
|
8568
|
+
retry: "Riprova",
|
|
8569
|
+
loading: "Caricamento delle cartelle…",
|
|
8570
|
+
adopting: "Apertura del workspace…",
|
|
8571
|
+
empty: "Nessuna sottocartella in questa cartella",
|
|
8572
|
+
truncated: "Sono mostrate solo le prime cartelle; inserisci un percorso completo per aprirne un’altra",
|
|
8573
|
+
"bad-path": "Inserisci un percorso assoluto del computer, come C:\\projects o /home/user/projects",
|
|
8574
|
+
unavailable: "Impossibile leggere questa cartella. Controlla il percorso e i permessi, poi riprova.",
|
|
8575
|
+
unauthorized: "Autorizzazione non disponibile. Chiudi questa finestra e riconnettiti al computer.",
|
|
8576
|
+
"invalid-response": "Risposta della cartella non disponibile. Riprova o riconnettiti al computer.",
|
|
8577
|
+
"request-failed": "Impossibile caricare la cartella. Controlla la connessione e riprova.",
|
|
8578
|
+
create: "Nuova cartella",
|
|
8579
|
+
createTitle: "Nome della nuova cartella",
|
|
8580
|
+
createAction: "Crea",
|
|
8581
|
+
createInvalid: "Inserisci un singolo nome di cartella senza separatori di percorso.",
|
|
8582
|
+
createExists: "Esiste già una cartella con questo nome.",
|
|
8583
|
+
createFailed: "Impossibile creare la cartella. Controlla i permessi di scrittura e riprova.",
|
|
8584
|
+
timeout: "Caricamento scaduto. Riprova o annulla.",
|
|
8585
|
+
failedOpen: "Impossibile aprire la selezione della cartella. Riconnettiti e riprova.",
|
|
8586
|
+
createParent: "Crea in",
|
|
8587
|
+
creating: "Creazione della cartella…",
|
|
8588
|
+
createStopping: "Chiusura della richiesta di creazione…",
|
|
8589
|
+
createUnknown: "Esito della creazione sconosciuto: la cartella potrebbe già esistere. Torna indietro e ricarica la directory prima di riprovare."
|
|
8590
|
+
}
|
|
8591
|
+
};
|
|
8592
|
+
/** Authenticated directory browsing/creation route; no native picker or Host service is replaced. */
|
|
8593
|
+
const WORKSPACE_DIRECTORIES_PATH = "/mobile-access/workspace-directories";
|
|
8594
|
+
/** Ask only this plugin's live directory dialog to handle a routed native Back. */
|
|
8595
|
+
function dismissMobileDirectoryPicker(dialog) {
|
|
8596
|
+
if (dialog.tagName !== "DIALOG" || dialog.getAttribute("data-dshm-picker-dialog") !== "true" || dialog.getAttribute("open") === null) return false;
|
|
8597
|
+
const dismiss = new Event("dsh-mobile:directory-picker-dismiss", { cancelable: true });
|
|
8598
|
+
dialog.dispatchEvent(dismiss);
|
|
8599
|
+
return dismiss.defaultPrevented;
|
|
8600
|
+
}
|
|
8601
|
+
function localeOf(doc) {
|
|
8602
|
+
const languages = [doc.documentElement.lang, ...doc.defaultView?.navigator.languages ?? []];
|
|
8603
|
+
for (const language of languages) {
|
|
8604
|
+
const code = language.toLowerCase().split("-")[0];
|
|
8605
|
+
if (code === "zh" || code === "en" || code === "it") return code;
|
|
8606
|
+
}
|
|
8607
|
+
return "en";
|
|
8608
|
+
}
|
|
8609
|
+
/** Create one child directory through the same authenticated route that lists them. */
|
|
8610
|
+
async function createDirectory(path, name, signal) {
|
|
8611
|
+
const response = await fetch(WORKSPACE_DIRECTORIES_PATH, {
|
|
8612
|
+
method: "POST",
|
|
8613
|
+
credentials: "same-origin",
|
|
8614
|
+
cache: "no-store",
|
|
8615
|
+
headers: { "content-type": "application/json" },
|
|
8616
|
+
body: JSON.stringify({
|
|
8617
|
+
path,
|
|
8618
|
+
name
|
|
8619
|
+
}),
|
|
8620
|
+
signal
|
|
8621
|
+
});
|
|
8622
|
+
if (!response.ok) {
|
|
8623
|
+
const code = response.status === 409 ? "directory-exists" : response.status === 400 ? "directory-create-failed" : response.status === 401 || response.status === 403 ? "unauthorized" : "directory-create-failed";
|
|
8624
|
+
try {
|
|
8625
|
+
await response.body?.cancel();
|
|
8626
|
+
} catch (error) {
|
|
8627
|
+
throw new MobileDirectoryCreateError(code);
|
|
8628
|
+
}
|
|
8629
|
+
throw new MobileDirectoryCreateError(code);
|
|
8630
|
+
}
|
|
8631
|
+
const payload = await response.json();
|
|
8632
|
+
if (typeof payload?.path !== "string" || !isMobileDirectoryPath(payload.path)) throw new MobileDirectoryCreateError("invalid-response");
|
|
8633
|
+
return payload.path;
|
|
8634
|
+
}
|
|
8635
|
+
async function readDirectories(path, signal) {
|
|
8636
|
+
const query = path === null ? "" : `?path=${encodeURIComponent(path)}`;
|
|
8637
|
+
const response = await fetch(`${WORKSPACE_DIRECTORIES_PATH}${query}`, {
|
|
8638
|
+
credentials: "same-origin",
|
|
8639
|
+
cache: "no-store",
|
|
8640
|
+
signal
|
|
8641
|
+
});
|
|
8642
|
+
if (!response.ok) {
|
|
8643
|
+
const code = response.status === 400 ? "bad-path" : response.status === 401 || response.status === 403 ? "unauthorized" : "unavailable";
|
|
8644
|
+
try {
|
|
8645
|
+
await response.body?.cancel();
|
|
8646
|
+
} catch (error) {
|
|
8647
|
+
throw new MobileDirectoryReadError(code, { cause: error });
|
|
8648
|
+
}
|
|
8649
|
+
throw new MobileDirectoryReadError(code);
|
|
8650
|
+
}
|
|
8651
|
+
try {
|
|
8652
|
+
return await response.json();
|
|
8653
|
+
} catch (error) {
|
|
8654
|
+
throw new MobileDirectoryReadError("invalid-response", { cause: error });
|
|
8655
|
+
}
|
|
8656
|
+
}
|
|
8657
|
+
const STYLES = `
|
|
8658
|
+
dialog.dshm-picker{position:fixed;inset:0;box-sizing:border-box;width:100%;max-width:none;height:100%;max-height:none;margin:0;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));border:0;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:400 14px/1.5 system-ui;overflow:hidden}
|
|
8659
|
+
dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::backdrop{background:rgb(0 0 0 / 42%)}
|
|
8660
|
+
.dshm-picker__panel{display:flex;flex-direction:column;box-sizing:border-box;width:min(680px,100%);height:min(720px,100%);min-height:0;overflow:hidden;border:0;border-radius:20px;box-shadow:0 0 0 .5px rgba(0 0 0 / 16%),0 3px 8px rgba(0 0 0 / 4%);background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
8661
|
+
.dshm-picker__header{display:flex;align-items:center;flex:none;gap:8px;padding:4px 8px 4px 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#dde0e4)}
|
|
8662
|
+
.dshm-picker__title{flex:1;min-width:0;margin:0;font-size:16px;font-weight:510;line-height:24px;overflow-wrap:anywhere}
|
|
8663
|
+
.dshm-picker__toolbar{display:flex;align-items:center;flex-wrap:nowrap;gap:8px;flex:none;padding:8px 10px}.dshm-picker__path-label{flex:1;min-width:0;display:flex;color:var(--dsw-alias-label-secondary,#606873)}
|
|
8664
|
+
.dshm-picker__path-caption{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
8665
|
+
.dshm-picker__path{box-sizing:border-box;width:100%;min-width:0;min-height:44px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:400 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
|
8666
|
+
.dshm-picker__path[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#b42318)}.dshm-picker__go{align-self:flex-end}
|
|
8667
|
+
.dshm-picker__navigation{display:flex;flex:none;gap:8px;padding:0}
|
|
8668
|
+
.dshm-picker__list{flex:1;min-height:0;margin:0;padding:0 10px 4px;overflow:auto;overscroll-behavior:contain;list-style:none;scroll-padding:4px}
|
|
8669
|
+
.dshm-picker__row{display:flex;align-items:center;gap:8px;box-sizing:border-box;width:100%;min-height:44px;padding:6px 8px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
|
|
8670
|
+
.dshm-picker__folder{flex:none;width:20px;height:20px;color:var(--dsw-alias-label-secondary,#606873)}
|
|
8671
|
+
.dshm-picker__name{flex:1;min-width:0;overflow-wrap:anywhere}.dshm-picker__row:hover:not(:disabled),.dshm-picker__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eef1f5)}
|
|
8672
|
+
.dshm-picker__row:active:not(:disabled),.dshm-picker__button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#e4e8ee)}
|
|
8673
|
+
.dshm-picker__button{flex:none;box-sizing:border-box;min-width:44px;min-height:44px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:transparent;color:inherit;font:500 14px/1.4 system-ui;cursor:pointer;touch-action:manipulation}
|
|
8674
|
+
.dshm-picker__icon{display:inline-flex;align-items:center;justify-content:center;width:44px;padding:0;border-color:transparent}.dshm-picker__icon>svg{display:block;width:18px;height:18px}
|
|
8675
|
+
.dshm-picker :is(button,input):focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dshm-picker button:disabled{opacity:.5;cursor:default}
|
|
8676
|
+
.dshm-picker [hidden]{display:none!important}.dshm-picker__create{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:12px 16px}
|
|
8677
|
+
.dshm-picker__create-title{margin:0;font-size:13px;color:var(--dsw-alias-label-secondary,#606873)}
|
|
8678
|
+
.dshm-picker__create-input{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-size:16px;line-height:1.4}
|
|
8679
|
+
.dshm-picker__create-actions{display:flex;justify-content:flex-end;gap:8px}.dshm-picker__create-error{color:var(--dsw-alias-state-error-primary,#b42318)}
|
|
8680
|
+
.dshm-picker__create-trigger{margin-right:auto;border-color:transparent;padding-left:8px;padding-right:8px}
|
|
8681
|
+
.dshm-picker__status{flex:none;margin:0;padding:4px 10px;font-size:13px;color:var(--dsw-alias-label-secondary,#606873);overflow-wrap:anywhere}.dshm-picker__error{color:var(--dsw-alias-state-error-primary,#b42318)}
|
|
8682
|
+
.dshm-picker__status[hidden],.dshm-picker__button[hidden]{display:none}.dshm-picker__footer{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;flex:none;gap:8px;padding:6px 10px;border-top:1px solid var(--dsw-alias-border-l2,#dde0e4)}
|
|
8683
|
+
.dshm-picker__confirm{min-width:72px;border-color:transparent;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff)}.dshm-picker__confirm:hover:not(:disabled),.dshm-picker__confirm:active:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}
|
|
8684
|
+
@media(max-width:420px){dialog.dshm-picker{padding:max(8px,env(safe-area-inset-top)) max(8px,env(safe-area-inset-right)) max(8px,env(safe-area-inset-bottom)) max(8px,env(safe-area-inset-left))}}
|
|
8685
|
+
`;
|
|
8686
|
+
/**
|
|
8687
|
+
* Whether DSH's own browse directory picker is already mounted on this page.
|
|
8688
|
+
*
|
|
8689
|
+
* DSH's adaptive `directory-picker` resolves to
|
|
8690
|
+
* `@deepseek-ai/dsh-host-directory-picker-auto`, which mounts the Host backend
|
|
8691
|
+
* and the Client face together and picks `browse` whenever the host is remotely
|
|
8692
|
+
* reachable. Its client face fills the same directory-flow slots this plugin
|
|
8693
|
+
* would use, so a plugin-owned picker would shadow the upstream one and take
|
|
8694
|
+
* away its folder creation. The desktop keeps the native OS chooser; a remote
|
|
8695
|
+
* browser can only reach the web implementation.
|
|
8696
|
+
*
|
|
8697
|
+
* @returns true when the upstream browse surface owns those slots on this page.
|
|
8698
|
+
*/
|
|
8699
|
+
function hasUpstreamBrowsePicker(view) {
|
|
8700
|
+
if (!("__DSH_BOOT__" in view)) return false;
|
|
8701
|
+
const graph = view.__DSH_BOOT__;
|
|
8702
|
+
if (typeof graph !== "object" || graph === null || !("entries" in graph) || !Array.isArray(graph.entries)) return false;
|
|
8703
|
+
return graph.entries.some((entry) => typeof entry === "object" && entry !== null && "id" in entry && entry.id === "@deepseek-ai/dsh-client-ui-directory-picker-browse");
|
|
8704
|
+
}
|
|
8705
|
+
function openDirectoryDialog(doc, getProps, trackDisposal) {
|
|
8706
|
+
const view = doc.defaultView;
|
|
8707
|
+
if (view === null) throw new Error("Directory picker requires a browser window");
|
|
8708
|
+
const copy = MESSAGES[localeOf(doc)];
|
|
8709
|
+
const active = doc.activeElement;
|
|
8710
|
+
const activeText = active instanceof HTMLElement && (active.isContentEditable || active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement);
|
|
8711
|
+
const trigger = active instanceof HTMLElement && !activeText ? active : void 0;
|
|
8712
|
+
if (activeText) active.blur();
|
|
8713
|
+
const dialog = doc.createElement("dialog");
|
|
8714
|
+
dialog.className = "dshm-picker";
|
|
8715
|
+
dialog.lang = localeOf(doc);
|
|
8716
|
+
dialog.setAttribute("role", "dialog");
|
|
8717
|
+
dialog.setAttribute("aria-modal", "true");
|
|
8718
|
+
dialog.setAttribute("aria-label", copy.title);
|
|
8719
|
+
dialog.setAttribute("data-dshm-picker-dialog", "true");
|
|
8720
|
+
const panel = doc.createElement("section");
|
|
8721
|
+
panel.className = "dshm-picker__panel";
|
|
8722
|
+
const button = (text, name) => {
|
|
8723
|
+
const value = doc.createElement("button");
|
|
8724
|
+
value.type = "button";
|
|
8725
|
+
value.textContent = text;
|
|
8726
|
+
value.className = "dshm-picker__button";
|
|
8727
|
+
if (name !== void 0) value.setAttribute(`data-dshm-picker-${name}`, "");
|
|
8728
|
+
return value;
|
|
8729
|
+
};
|
|
8730
|
+
const iconButton = (label, outline, name) => {
|
|
8731
|
+
const value = button("", name);
|
|
8732
|
+
value.classList.add("dshm-picker__icon");
|
|
8733
|
+
value.setAttribute("aria-label", label);
|
|
8734
|
+
value.title = label;
|
|
8735
|
+
const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
8736
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
8737
|
+
icon.setAttribute("fill", "none");
|
|
8738
|
+
icon.setAttribute("stroke", "currentColor");
|
|
8739
|
+
icon.setAttribute("stroke-width", "1.75");
|
|
8740
|
+
icon.setAttribute("stroke-linecap", "round");
|
|
8741
|
+
icon.setAttribute("stroke-linejoin", "round");
|
|
8742
|
+
icon.setAttribute("aria-hidden", "true");
|
|
8743
|
+
const shape = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
8744
|
+
shape.setAttribute("d", outline);
|
|
8745
|
+
icon.append(shape);
|
|
8746
|
+
value.append(icon);
|
|
8747
|
+
return value;
|
|
8748
|
+
};
|
|
8749
|
+
const header = doc.createElement("header");
|
|
8750
|
+
header.className = "dshm-picker__header";
|
|
8751
|
+
const title = doc.createElement("h2");
|
|
8752
|
+
title.className = "dshm-picker__title";
|
|
8753
|
+
title.textContent = copy.title;
|
|
8754
|
+
const close = iconButton(copy.close, "M6 6l12 12M6 18L18 6");
|
|
8755
|
+
header.append(title, close);
|
|
8756
|
+
const toolbar = doc.createElement("form");
|
|
8757
|
+
toolbar.className = "dshm-picker__toolbar";
|
|
8758
|
+
const pathLabel = doc.createElement("label");
|
|
8759
|
+
pathLabel.className = "dshm-picker__path-label";
|
|
8760
|
+
const pathCaption = doc.createElement("span");
|
|
8761
|
+
pathCaption.className = "dshm-picker__path-caption";
|
|
8762
|
+
pathCaption.textContent = copy.path;
|
|
8763
|
+
pathLabel.append(pathCaption);
|
|
8764
|
+
const path = doc.createElement("input");
|
|
8765
|
+
path.type = "text";
|
|
8766
|
+
path.className = "dshm-picker__path";
|
|
8767
|
+
path.setAttribute("data-dshm-picker-path", "");
|
|
8768
|
+
path.spellcheck = false;
|
|
8769
|
+
path.autocomplete = "off";
|
|
8770
|
+
path.maxLength = 8192;
|
|
8771
|
+
pathLabel.append(path);
|
|
8772
|
+
const go = iconButton(copy.go, "M5 12h14m-6-6 6 6-6 6", "load");
|
|
8773
|
+
go.type = "submit";
|
|
8774
|
+
go.classList.add("dshm-picker__go");
|
|
8775
|
+
toolbar.append(pathLabel, go);
|
|
8776
|
+
const navigation = doc.createElement("nav");
|
|
8777
|
+
navigation.className = "dshm-picker__navigation";
|
|
8778
|
+
navigation.setAttribute("aria-label", copy.title);
|
|
8779
|
+
const home = iconButton(copy.home, "m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1Z", "home");
|
|
8780
|
+
const up = iconButton(copy.up, "M12 20V4m-7 7 7-7 7 7", "up");
|
|
8781
|
+
navigation.append(home, up);
|
|
8782
|
+
toolbar.prepend(navigation);
|
|
8783
|
+
const status = doc.createElement("p");
|
|
8784
|
+
status.className = "dshm-picker__status";
|
|
8785
|
+
status.setAttribute("role", "status");
|
|
8786
|
+
status.setAttribute("aria-live", "polite");
|
|
8787
|
+
const error = doc.createElement("p");
|
|
8788
|
+
error.className = "dshm-picker__status dshm-picker__error";
|
|
8789
|
+
error.setAttribute("role", "alert");
|
|
8790
|
+
error.hidden = true;
|
|
8791
|
+
const list = doc.createElement("ul");
|
|
8792
|
+
list.className = "dshm-picker__list";
|
|
8793
|
+
list.setAttribute("aria-label", copy.title);
|
|
8794
|
+
const footer = doc.createElement("footer");
|
|
8795
|
+
footer.className = "dshm-picker__footer";
|
|
8796
|
+
const retry = button(copy.retry, "retry");
|
|
8797
|
+
retry.hidden = true;
|
|
8798
|
+
const create = button(copy.create, "create");
|
|
8799
|
+
create.classList.add("dshm-picker__create-trigger");
|
|
8800
|
+
const cancel = button(copy.cancel, "cancel");
|
|
8801
|
+
const confirm = button(copy.open, "confirm");
|
|
8802
|
+
confirm.classList.add("dshm-picker__confirm");
|
|
8803
|
+
const createPanel = doc.createElement("div");
|
|
8804
|
+
createPanel.className = "dshm-picker__create";
|
|
8805
|
+
createPanel.hidden = true;
|
|
8806
|
+
const createTitle = doc.createElement("label");
|
|
8807
|
+
createTitle.className = "dshm-picker__create-title";
|
|
8808
|
+
createTitle.textContent = copy.createTitle;
|
|
8809
|
+
const createInput = doc.createElement("input");
|
|
8810
|
+
createInput.type = "text";
|
|
8811
|
+
createInput.className = "dshm-picker__create-input";
|
|
8812
|
+
createInput.setAttribute("data-dshm-picker-create-name", "");
|
|
8813
|
+
createInput.spellcheck = false;
|
|
8814
|
+
createInput.autocomplete = "off";
|
|
8815
|
+
createInput.maxLength = 255;
|
|
8816
|
+
createTitle.append(createInput);
|
|
8817
|
+
const createParent = doc.createElement("p");
|
|
8818
|
+
createParent.className = "dshm-picker__status";
|
|
8819
|
+
const createStatus = doc.createElement("p");
|
|
8820
|
+
createStatus.className = "dshm-picker__status";
|
|
8821
|
+
createStatus.setAttribute("role", "status");
|
|
8822
|
+
createStatus.setAttribute("aria-live", "polite");
|
|
8823
|
+
const createError = doc.createElement("p");
|
|
8824
|
+
createError.className = "dshm-picker__status dshm-picker__create-error";
|
|
8825
|
+
createError.setAttribute("role", "alert");
|
|
8826
|
+
createError.setAttribute("data-dshm-picker-create-error", "");
|
|
8827
|
+
createError.hidden = true;
|
|
8828
|
+
const createActions = doc.createElement("div");
|
|
8829
|
+
createActions.className = "dshm-picker__create-actions";
|
|
8830
|
+
const createCancel = button(copy.cancel, "create-cancel");
|
|
8831
|
+
const createConfirm = button(copy.createAction, "create-confirm");
|
|
8832
|
+
createActions.append(createCancel, createConfirm);
|
|
8833
|
+
createPanel.append(createParent, createTitle, createError, createStatus, createActions);
|
|
8834
|
+
footer.append(create, retry, cancel, confirm);
|
|
8835
|
+
panel.append(header, toolbar, status, error, list, createPanel, footer);
|
|
8836
|
+
dialog.append(panel);
|
|
8837
|
+
const browser = new MobileDirectoryBrowser(readDirectories);
|
|
8838
|
+
const creator = new MobileDirectoryCreator(createDirectory, (created) => {
|
|
8839
|
+
browser.navigate(created);
|
|
8840
|
+
});
|
|
8841
|
+
let creationNotice = false;
|
|
8842
|
+
let creationNoticeListing = null;
|
|
8843
|
+
const closeCreate = () => {
|
|
8844
|
+
const child = creator.getSnapshot();
|
|
8845
|
+
if (child.pending || child.error === "timeout" || child.error === "request-failed" || child.error === "invalid-response") {
|
|
8846
|
+
creationNotice = true;
|
|
8847
|
+
creationNoticeListing = browser.getSnapshot().listing;
|
|
8848
|
+
}
|
|
8849
|
+
if (creator.close()) {
|
|
8850
|
+
if (create.disabled) close.focus();
|
|
8851
|
+
else create.focus();
|
|
8852
|
+
}
|
|
8853
|
+
};
|
|
8854
|
+
create.addEventListener("click", () => {
|
|
8855
|
+
const parent = browser.getSnapshot().listing?.path;
|
|
8856
|
+
if (!browser.canConfirm() || parent === void 0 || !creator.open(parent)) return;
|
|
8857
|
+
creationNotice = false;
|
|
8858
|
+
createInput.focus();
|
|
8859
|
+
createInput.select();
|
|
8860
|
+
});
|
|
8861
|
+
createCancel.addEventListener("click", closeCreate);
|
|
8862
|
+
createConfirm.addEventListener("click", () => {
|
|
8863
|
+
creator.submit();
|
|
8864
|
+
});
|
|
8865
|
+
createInput.addEventListener("input", () => {
|
|
8866
|
+
creator.setName(createInput.value);
|
|
8867
|
+
});
|
|
8868
|
+
createInput.addEventListener("keydown", (event) => {
|
|
8869
|
+
if (event.key === "Enter" && !event.isComposing) {
|
|
8870
|
+
event.preventDefault();
|
|
8871
|
+
creator.submit();
|
|
8872
|
+
}
|
|
8873
|
+
});
|
|
8874
|
+
let disposed = false;
|
|
8875
|
+
let closedByOwner = false;
|
|
8876
|
+
let renderedListing;
|
|
8877
|
+
let disposal;
|
|
8878
|
+
const render = () => {
|
|
8879
|
+
if (disposed) return;
|
|
8880
|
+
const state = browser.getSnapshot();
|
|
8881
|
+
if (creationNotice && state.listing !== creationNoticeListing) creationNotice = false;
|
|
8882
|
+
const child = creator.getSnapshot();
|
|
8883
|
+
const adopting = state.committed || state.hostBusy;
|
|
8884
|
+
const blocked = adopting || state.creating;
|
|
8885
|
+
path.disabled = blocked;
|
|
8886
|
+
if (path.value !== state.draft) path.value = state.draft;
|
|
8887
|
+
path.setAttribute("aria-invalid", String(state.error === "bad-path"));
|
|
8888
|
+
go.disabled = blocked || state.phase === "loading" || !isMobileDirectoryPath(state.draft);
|
|
8889
|
+
home.disabled = blocked;
|
|
8890
|
+
up.disabled = blocked || state.listing?.parent === void 0;
|
|
8891
|
+
confirm.disabled = !browser.canConfirm();
|
|
8892
|
+
cancel.disabled = adopting;
|
|
8893
|
+
close.disabled = adopting;
|
|
8894
|
+
retry.hidden = state.phase !== "error" || state.error === "bad-path";
|
|
8895
|
+
retry.disabled = blocked;
|
|
8896
|
+
create.disabled = !browser.canConfirm();
|
|
8897
|
+
title.textContent = child.open ? copy.create : copy.title;
|
|
8898
|
+
toolbar.hidden = child.open;
|
|
8899
|
+
navigation.hidden = child.open;
|
|
8900
|
+
list.hidden = child.open;
|
|
8901
|
+
footer.hidden = child.open;
|
|
8902
|
+
createPanel.hidden = !child.open;
|
|
8903
|
+
if (createInput.value !== child.name) createInput.value = child.name;
|
|
8904
|
+
createInput.disabled = child.pending;
|
|
8905
|
+
createInput.setAttribute("aria-invalid", String(child.error === "invalid-name"));
|
|
8906
|
+
createConfirm.disabled = child.pending || !isMobileDirectoryName(child.name.trim());
|
|
8907
|
+
createParent.textContent = `${copy.createParent}: ${child.parent ?? ""}`;
|
|
8908
|
+
createError.textContent = child.error === null ? "" : child.error === "invalid-name" ? copy.createInvalid : child.error === "directory-exists" ? copy.createExists : child.error === "directory-create-failed" ? copy.createFailed : child.error === "unauthorized" ? copy.unauthorized : copy.createUnknown;
|
|
8909
|
+
createError.hidden = child.error === null;
|
|
8910
|
+
createStatus.textContent = child.pending && child.error === null ? copy.creating : "";
|
|
8911
|
+
createStatus.hidden = createStatus.textContent === "";
|
|
8912
|
+
status.textContent = child.pending ? copy.createStopping : creationNotice ? copy.createUnknown : adopting ? copy.adopting : state.phase === "loading" ? copy.loading : state.listing?.truncated === true ? copy.truncated : state.listing?.entries.length === 0 ? copy.empty : "";
|
|
8913
|
+
status.hidden = child.open || status.textContent === "";
|
|
8914
|
+
error.textContent = state.error === null ? "" : copy[state.error];
|
|
8915
|
+
error.hidden = child.open || state.error === null;
|
|
8916
|
+
list.setAttribute("aria-busy", String(state.phase === "loading"));
|
|
8917
|
+
if (renderedListing !== state.listing) {
|
|
8918
|
+
renderedListing = state.listing;
|
|
8919
|
+
list.replaceChildren();
|
|
8920
|
+
for (const entry of state.listing?.entries ?? []) {
|
|
8921
|
+
const item = doc.createElement("li");
|
|
8922
|
+
const row = button("");
|
|
8923
|
+
row.className = "dshm-picker__row";
|
|
8924
|
+
const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
8925
|
+
icon.setAttribute("class", "dshm-picker__folder");
|
|
8926
|
+
icon.setAttribute("aria-hidden", "true");
|
|
8927
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
8928
|
+
icon.setAttribute("fill", "none");
|
|
8929
|
+
icon.setAttribute("stroke", "currentColor");
|
|
8930
|
+
icon.setAttribute("stroke-width", "1.5");
|
|
8931
|
+
const outline = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
8932
|
+
outline.setAttribute("d", "M3 7V6a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z");
|
|
8933
|
+
icon.append(outline);
|
|
8934
|
+
const name = doc.createElement("span");
|
|
8935
|
+
name.className = "dshm-picker__name";
|
|
8936
|
+
name.textContent = entry.name;
|
|
8937
|
+
row.append(icon, name);
|
|
8938
|
+
row.addEventListener("click", () => {
|
|
8939
|
+
browser.navigate(entry.path);
|
|
8940
|
+
});
|
|
8941
|
+
item.append(row);
|
|
8942
|
+
list.append(item);
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
for (const row of list.querySelectorAll("button")) row.disabled = blocked;
|
|
8946
|
+
};
|
|
8947
|
+
const unsubscribe = browser.subscribe(render);
|
|
8948
|
+
const unsubscribeCreate = creator.subscribe(() => {
|
|
8949
|
+
const child = creator.getSnapshot();
|
|
8950
|
+
browser.setCreating(child.open || child.pending);
|
|
8951
|
+
render();
|
|
8952
|
+
});
|
|
8953
|
+
const requestClose = () => {
|
|
8954
|
+
if (creator.getSnapshot().open) {
|
|
8955
|
+
closeCreate();
|
|
8956
|
+
return;
|
|
8957
|
+
}
|
|
8958
|
+
if (disposed || !browser.cancel()) return;
|
|
8959
|
+
creator.close();
|
|
8960
|
+
closedByOwner = true;
|
|
8961
|
+
if (dialog.open) dialog.close();
|
|
8962
|
+
getProps().onCancel();
|
|
8963
|
+
};
|
|
8964
|
+
const onBack = (event) => {
|
|
8965
|
+
if (!event.cancelable || event.defaultPrevented || !dialog.open) return;
|
|
8966
|
+
event.preventDefault();
|
|
8967
|
+
event.stopImmediatePropagation();
|
|
8968
|
+
requestClose();
|
|
8969
|
+
};
|
|
8970
|
+
const onEscape = (event) => {
|
|
8971
|
+
if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open) return;
|
|
8972
|
+
event.preventDefault();
|
|
8973
|
+
event.stopImmediatePropagation();
|
|
8974
|
+
requestClose();
|
|
8975
|
+
};
|
|
8976
|
+
const dispose = () => {
|
|
8977
|
+
if (disposal !== void 0) return disposal;
|
|
8978
|
+
disposed = true;
|
|
8979
|
+
unsubscribe();
|
|
8980
|
+
unsubscribeCreate();
|
|
8981
|
+
const creationStopped = creator.dispose();
|
|
8982
|
+
const stopped = browser.dispose();
|
|
8983
|
+
const failures = [];
|
|
8984
|
+
const clean = (action) => {
|
|
8985
|
+
try {
|
|
8986
|
+
action();
|
|
8987
|
+
} catch (error) {
|
|
8988
|
+
failures.push(error);
|
|
8989
|
+
}
|
|
8990
|
+
};
|
|
8991
|
+
clean(() => {
|
|
8992
|
+
view.removeEventListener("dsh-mobile:native-back", onBack, true);
|
|
8993
|
+
});
|
|
8994
|
+
clean(() => {
|
|
8995
|
+
view.removeEventListener("keydown", onEscape, true);
|
|
8996
|
+
});
|
|
8997
|
+
closedByOwner = true;
|
|
8998
|
+
clean(() => {
|
|
8999
|
+
if (dialog.open) dialog.close();
|
|
9000
|
+
});
|
|
9001
|
+
clean(() => {
|
|
9002
|
+
dialog.remove();
|
|
9003
|
+
});
|
|
9004
|
+
clean(() => {
|
|
9005
|
+
if (trigger?.isConnected) trigger.focus({ preventScroll: true });
|
|
9006
|
+
});
|
|
9007
|
+
disposal = (async () => {
|
|
9008
|
+
for (const result of await Promise.allSettled([stopped, creationStopped])) if (result.status === "rejected") failures.push(result.reason);
|
|
9009
|
+
if (failures.length > 0) throw new AggregateError(failures, "Mobile directory dialog disposal failed");
|
|
9010
|
+
})();
|
|
9011
|
+
return disposal;
|
|
9012
|
+
};
|
|
9013
|
+
toolbar.addEventListener("submit", (event) => {
|
|
9014
|
+
event.preventDefault();
|
|
9015
|
+
browser.navigate(path.value);
|
|
9016
|
+
});
|
|
9017
|
+
path.addEventListener("input", () => {
|
|
9018
|
+
browser.setDraft(path.value);
|
|
9019
|
+
});
|
|
9020
|
+
home.addEventListener("click", () => {
|
|
9021
|
+
browser.navigate(null);
|
|
9022
|
+
});
|
|
9023
|
+
up.addEventListener("click", () => {
|
|
9024
|
+
const parent = browser.getSnapshot().listing?.parent;
|
|
9025
|
+
if (parent !== void 0) browser.navigate(parent);
|
|
9026
|
+
});
|
|
9027
|
+
retry.addEventListener("click", () => {
|
|
9028
|
+
browser.retry();
|
|
9029
|
+
});
|
|
9030
|
+
cancel.addEventListener("click", requestClose);
|
|
9031
|
+
close.addEventListener("click", requestClose);
|
|
9032
|
+
confirm.addEventListener("click", () => {
|
|
9033
|
+
const picked = browser.confirm();
|
|
9034
|
+
if (picked !== void 0) getProps().onPicked(picked);
|
|
9035
|
+
});
|
|
9036
|
+
dialog.addEventListener("cancel", (event) => {
|
|
9037
|
+
event.preventDefault();
|
|
9038
|
+
requestClose();
|
|
9039
|
+
});
|
|
9040
|
+
dialog.addEventListener("dsh-mobile:directory-picker-dismiss", (event) => {
|
|
9041
|
+
if (!dialog.open || disposed || !event.cancelable) return;
|
|
9042
|
+
event.preventDefault();
|
|
9043
|
+
requestClose();
|
|
9044
|
+
});
|
|
9045
|
+
dialog.addEventListener("click", (event) => {
|
|
9046
|
+
if (event.target === dialog) requestClose();
|
|
9047
|
+
});
|
|
9048
|
+
dialog.addEventListener("close", () => {
|
|
9049
|
+
if (!disposed && !closedByOwner && browser.cancel()) getProps().onCancel();
|
|
9050
|
+
});
|
|
9051
|
+
view.addEventListener("dsh-mobile:native-back", onBack, true);
|
|
9052
|
+
view.addEventListener("keydown", onEscape, true);
|
|
9053
|
+
try {
|
|
9054
|
+
doc.body.append(dialog);
|
|
9055
|
+
dialog.showModal();
|
|
9056
|
+
close.focus({ preventScroll: true });
|
|
9057
|
+
browser.start();
|
|
9058
|
+
browser.setHostBusy(getProps().busy === true);
|
|
9059
|
+
} catch (error) {
|
|
9060
|
+
trackDisposal(dispose());
|
|
9061
|
+
throw error;
|
|
9062
|
+
}
|
|
9063
|
+
return {
|
|
9064
|
+
setHostBusy: (value) => {
|
|
9065
|
+
browser.setHostBusy(value);
|
|
9066
|
+
},
|
|
9067
|
+
dispose
|
|
9068
|
+
};
|
|
9069
|
+
}
|
|
9070
|
+
/** Register two public directory flows and drain their read work before removing styles. */
|
|
9071
|
+
function installMobileDirectoryPicker(ctx, doc) {
|
|
9072
|
+
const style = doc.createElement("style");
|
|
9073
|
+
style.dataset.plugin = "dsh-mobile-directory-picker";
|
|
9074
|
+
style.textContent = STYLES;
|
|
9075
|
+
doc.head.append(style);
|
|
9076
|
+
const owners = /* @__PURE__ */ new Set();
|
|
9077
|
+
const pending = /* @__PURE__ */ new Set();
|
|
9078
|
+
const cleanupFailures = [];
|
|
9079
|
+
let disposed = false;
|
|
9080
|
+
const trackDisposal = (task) => {
|
|
9081
|
+
pending.add(task);
|
|
9082
|
+
task.then(() => {
|
|
9083
|
+
pending.delete(task);
|
|
9084
|
+
}, (error) => {
|
|
9085
|
+
pending.delete(task);
|
|
9086
|
+
cleanupFailures.push(error);
|
|
9087
|
+
console.error("DSH Mobile directory picker cleanup failed");
|
|
9088
|
+
});
|
|
9089
|
+
};
|
|
9090
|
+
const retire = (owner) => {
|
|
9091
|
+
if (owners.delete(owner)) trackDisposal(owner.dispose());
|
|
9092
|
+
};
|
|
9093
|
+
function MobileDirectoryFlow(props) {
|
|
9094
|
+
const current = (0, react.useRef)(props);
|
|
9095
|
+
current.current = props;
|
|
9096
|
+
const owner = (0, react.useRef)(void 0);
|
|
9097
|
+
(0, react.useEffect)(() => {
|
|
9098
|
+
if (!props.open || disposed) return;
|
|
9099
|
+
try {
|
|
9100
|
+
const dialog = openDirectoryDialog(doc, () => current.current, trackDisposal);
|
|
9101
|
+
owners.add(dialog);
|
|
9102
|
+
owner.current = dialog;
|
|
9103
|
+
return () => {
|
|
9104
|
+
owner.current = void 0;
|
|
9105
|
+
retire(dialog);
|
|
9106
|
+
};
|
|
9107
|
+
} catch (error) {
|
|
9108
|
+
console.error("DSH Mobile directory picker could not open");
|
|
9109
|
+
current.current.onError(MESSAGES[localeOf(doc)].failedOpen);
|
|
9110
|
+
}
|
|
9111
|
+
}, [props.open]);
|
|
9112
|
+
(0, react.useEffect)(() => {
|
|
9113
|
+
owner.current?.setHostBusy(props.busy === true);
|
|
9114
|
+
}, [props.busy]);
|
|
9115
|
+
return null;
|
|
9116
|
+
}
|
|
9117
|
+
let remove;
|
|
9118
|
+
try {
|
|
9119
|
+
remove = ctx.slots.inject("sidebar.workspaces.directoryFlow", () => ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => {
|
|
9120
|
+
const removeSidebar = ctx.slots.register({
|
|
9121
|
+
name: "sidebar.workspaces.directoryFlow",
|
|
9122
|
+
id: "dsh-mobile-directory-picker",
|
|
9123
|
+
priority: -1
|
|
9124
|
+
}, MobileDirectoryFlow);
|
|
9125
|
+
let removeHero;
|
|
9126
|
+
try {
|
|
9127
|
+
removeHero = ctx.slots.register({
|
|
9128
|
+
name: "conversation.hero.workspace.directoryFlow",
|
|
9129
|
+
id: "dsh-mobile-directory-picker",
|
|
9130
|
+
priority: -1
|
|
9131
|
+
}, MobileDirectoryFlow);
|
|
9132
|
+
} catch (error) {
|
|
9133
|
+
removeSidebar();
|
|
9134
|
+
throw error;
|
|
9135
|
+
}
|
|
9136
|
+
return () => {
|
|
9137
|
+
removeHero();
|
|
9138
|
+
removeSidebar();
|
|
9139
|
+
};
|
|
9140
|
+
}));
|
|
9141
|
+
} catch (error) {
|
|
9142
|
+
style.remove();
|
|
9143
|
+
throw error;
|
|
9144
|
+
}
|
|
9145
|
+
return async () => {
|
|
9146
|
+
disposed = true;
|
|
9147
|
+
const failures = [];
|
|
9148
|
+
try {
|
|
9149
|
+
remove();
|
|
9150
|
+
} catch (error) {
|
|
9151
|
+
failures.push(error);
|
|
9152
|
+
}
|
|
9153
|
+
for (const owner of [...owners]) try {
|
|
9154
|
+
retire(owner);
|
|
9155
|
+
} catch (error) {
|
|
9156
|
+
failures.push(error);
|
|
9157
|
+
}
|
|
9158
|
+
await Promise.allSettled([...pending]);
|
|
9159
|
+
style.remove();
|
|
9160
|
+
failures.push(...cleanupFailures);
|
|
9161
|
+
if (failures.length > 0) throw new AggregateError(failures, "Mobile directory picker disposal failed");
|
|
9162
|
+
};
|
|
9163
|
+
}
|
|
9164
|
+
//#endregion
|
|
7893
9165
|
//#region src/client.ts
|
|
7894
9166
|
/** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
|
|
7895
9167
|
function parseMobileRemoteProvider(value) {
|
|
@@ -7944,6 +9216,7 @@ window.__ModuleLoader__.load({
|
|
|
7944
9216
|
const top = layers.at(-1);
|
|
7945
9217
|
if (top === void 0) return;
|
|
7946
9218
|
claim();
|
|
9219
|
+
if (dismissMobileDirectoryPicker(top)) return;
|
|
7947
9220
|
const modelEntry = [...modelPopups].find(([popup]) => popup.contains(top));
|
|
7948
9221
|
if (modelEntry !== void 0) {
|
|
7949
9222
|
const [modelPopup, trigger] = modelEntry;
|
|
@@ -13332,16 +14605,22 @@ window.__ModuleLoader__.load({
|
|
|
13332
14605
|
getLayout: () => ctx.get("layout"),
|
|
13333
14606
|
getSidebarRight: () => ctx.get("sidebarRight")
|
|
13334
14607
|
}));
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
14608
|
+
const removeDirectoryPicker = window.__DSH_MOBILE_FRONTEND__ === "dedicated" && !hasUpstreamBrowsePicker(window) ? installMobileDirectoryPicker(ctx, document) : () => Promise.resolve();
|
|
14609
|
+
return async () => {
|
|
14610
|
+
const removal = removeDirectoryPicker();
|
|
14611
|
+
try {
|
|
14612
|
+
disposeClientModuleDialogs();
|
|
14613
|
+
removeModuleSettings();
|
|
14614
|
+
removeSettingsAction();
|
|
14615
|
+
removeCustom();
|
|
14616
|
+
removeSurface();
|
|
14617
|
+
removeVoiceSession();
|
|
14618
|
+
removeDraftTransfer();
|
|
14619
|
+
localFont.dispose();
|
|
14620
|
+
style.remove();
|
|
14621
|
+
} finally {
|
|
14622
|
+
await removal;
|
|
14623
|
+
}
|
|
13345
14624
|
};
|
|
13346
14625
|
}
|
|
13347
14626
|
const removeControl = installDshLanguageBoundSurface(() => {
|