pinokiod 8.0.56 → 8.0.58
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/kernel/vault/automatic_scans.js +14 -4
- package/package.json +1 -1
- package/server/index.js +13 -0
- package/server/public/app-vault-mode.js +37 -0
- package/server/public/layout.js +137 -6
- package/server/public/vault.js +3 -3
- package/server/views/app.ejs +54 -10
- package/server/views/layout.ejs +15 -3
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +1 -1
- package/test/launch-requirements-browser.test.js +4 -4
- package/test/vault-automatic-scan-ui.test.js +305 -0
- package/test/vault-automatic-scans.test.js +17 -0
- package/test/vault-ui.test.js +20 -0
|
@@ -152,8 +152,9 @@ class AutomaticScans {
|
|
|
152
152
|
return setting && setting.mode === "manual" ? "manual" : "automatic"
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
broadcast() {
|
|
155
|
+
broadcast(completion = null) {
|
|
156
156
|
const snapshot = this.snapshot()
|
|
157
|
+
if (completion) snapshot.completion = completion
|
|
157
158
|
for (const listener of this.listeners) {
|
|
158
159
|
try {
|
|
159
160
|
listener(snapshot)
|
|
@@ -617,6 +618,7 @@ class AutomaticScans {
|
|
|
617
618
|
|
|
618
619
|
async precheckFinished(active, result, error) {
|
|
619
620
|
const app = active.app
|
|
621
|
+
let completion = null
|
|
620
622
|
try {
|
|
621
623
|
await this.withAppTransition(app, async () => {
|
|
622
624
|
let reason = active.reason
|
|
@@ -661,13 +663,20 @@ class AutomaticScans {
|
|
|
661
663
|
? Object.assign({}, this.settings.get(app))
|
|
662
664
|
: null
|
|
663
665
|
try {
|
|
664
|
-
await this.publishResultNow(app, result)
|
|
666
|
+
const outcome = await this.publishResultNow(app, result)
|
|
667
|
+
if (outcome === "empty") {
|
|
668
|
+
completion = {
|
|
669
|
+
app,
|
|
670
|
+
outcome: "no_possible_duplicates"
|
|
671
|
+
}
|
|
672
|
+
}
|
|
665
673
|
} catch (publicationError) {
|
|
666
674
|
this.restorePrevious(app)
|
|
667
675
|
throw publicationError
|
|
668
676
|
}
|
|
669
677
|
reason = active.reason
|
|
670
678
|
if (reason) {
|
|
679
|
+
completion = null
|
|
671
680
|
await this.restorePublishedState(app, entry, previousSetting)
|
|
672
681
|
this.entries.set(app, entry)
|
|
673
682
|
this.log("publication-reverted", { app, reason })
|
|
@@ -682,7 +691,7 @@ class AutomaticScans {
|
|
|
682
691
|
})
|
|
683
692
|
} finally {
|
|
684
693
|
if (this.active === active) this.active = null
|
|
685
|
-
this.broadcast()
|
|
694
|
+
this.broadcast(completion)
|
|
686
695
|
this.schedule()
|
|
687
696
|
}
|
|
688
697
|
}
|
|
@@ -764,7 +773,7 @@ class AutomaticScans {
|
|
|
764
773
|
possible_files: possibleFiles,
|
|
765
774
|
acknowledged: acknowledged === result.signature
|
|
766
775
|
})
|
|
767
|
-
return
|
|
776
|
+
return "result"
|
|
768
777
|
}
|
|
769
778
|
const acknowledged = (this.settings.get(app) || {})
|
|
770
779
|
.acknowledged_signature
|
|
@@ -777,6 +786,7 @@ class AutomaticScans {
|
|
|
777
786
|
}
|
|
778
787
|
this.entries.delete(app)
|
|
779
788
|
this.log("no-possible-matches", { app })
|
|
789
|
+
return "empty"
|
|
780
790
|
}
|
|
781
791
|
|
|
782
792
|
async clearAutomaticState(apps, reason, options = {}) {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1993,6 +1993,19 @@ class Server {
|
|
|
1993
1993
|
const vault = this.kernel.vault
|
|
1994
1994
|
if (vault && vault.ready) await vault.ready
|
|
1995
1995
|
result.vault_enabled = !!(vault && vault.enabled)
|
|
1996
|
+
result.vault_automatic_mode = "automatic"
|
|
1997
|
+
if (result.vault_enabled) {
|
|
1998
|
+
try {
|
|
1999
|
+
const snapshot = await vault.automaticScanStatus()
|
|
2000
|
+
const setting = Array.isArray(snapshot && snapshot.settings)
|
|
2001
|
+
? snapshot.settings.find((item) =>
|
|
2002
|
+
item && item.app === name)
|
|
2003
|
+
: null
|
|
2004
|
+
result.vault_automatic_mode = setting && setting.mode === "manual"
|
|
2005
|
+
? "manual"
|
|
2006
|
+
: "automatic"
|
|
2007
|
+
} catch (_) {}
|
|
2008
|
+
}
|
|
1996
2009
|
if (!registryEnabled) {
|
|
1997
2010
|
result.pendingSnapshotId = null
|
|
1998
2011
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const status = document.querySelector("[data-app-vault-mode]")
|
|
3
|
+
if (!status) return
|
|
4
|
+
|
|
5
|
+
const app = status.dataset.app || ""
|
|
6
|
+
const tab = status.closest("#save-space-tab")
|
|
7
|
+
const label = status.querySelector("[data-app-vault-mode-label]")
|
|
8
|
+
const setMode = (value) => {
|
|
9
|
+
const mode = value === "manual" ? "manual" : "automatic"
|
|
10
|
+
status.dataset.mode = mode
|
|
11
|
+
status.hidden = false
|
|
12
|
+
if (label) label.textContent = mode === "automatic" ? "Auto" : "Manual"
|
|
13
|
+
if (tab) {
|
|
14
|
+
tab.setAttribute("aria-label",
|
|
15
|
+
`Disk Saver — ${mode === "automatic" ? "Automatic" : "Manual"} checking`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const applySnapshot = (snapshot) => {
|
|
19
|
+
const settings = snapshot && Array.isArray(snapshot.settings)
|
|
20
|
+
? snapshot.settings
|
|
21
|
+
: []
|
|
22
|
+
const setting = settings.find((item) => item && item.app === app)
|
|
23
|
+
setMode(setting && setting.mode)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setMode(status.dataset.mode)
|
|
27
|
+
if (!app || typeof window.EventSource !== "function") return
|
|
28
|
+
|
|
29
|
+
const source = new window.EventSource(
|
|
30
|
+
"/info/vault/automatic-scans/events")
|
|
31
|
+
source.onmessage = (event) => {
|
|
32
|
+
try {
|
|
33
|
+
applySnapshot(JSON.parse(event.data))
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
}
|
|
36
|
+
window.addEventListener("beforeunload", () => source.close(), { once: true })
|
|
37
|
+
})()
|
package/server/public/layout.js
CHANGED
|
@@ -866,8 +866,16 @@
|
|
|
866
866
|
<circle cx="10" cy="10" r="7.5"></circle>
|
|
867
867
|
<path d="M8 7.25v5.5M12 7.25v5.5"></path>
|
|
868
868
|
</svg>`;
|
|
869
|
+
const COMPLETE_ICON = `
|
|
870
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
871
|
+
<circle cx="10" cy="10" r="7.5"></circle>
|
|
872
|
+
<path d="m6.75 10.1 2.1 2.1 4.6-4.65"></path>
|
|
873
|
+
</svg>`;
|
|
874
|
+
const COMPLETION_VISIBLE_MS = 4000;
|
|
869
875
|
|
|
870
876
|
let eventSource = null;
|
|
877
|
+
let visibleCheckingApps = new Set();
|
|
878
|
+
const completions = new Map();
|
|
871
879
|
|
|
872
880
|
function statusText(row) {
|
|
873
881
|
if (row.state === 'paused') {
|
|
@@ -912,6 +920,97 @@
|
|
|
912
920
|
tray.hidden = tray.childElementCount === 0;
|
|
913
921
|
}
|
|
914
922
|
|
|
923
|
+
function removeCompletion(app) {
|
|
924
|
+
const completion = completions.get(app);
|
|
925
|
+
if (!completion) return;
|
|
926
|
+
if (completion.timer) window.clearTimeout(completion.timer);
|
|
927
|
+
completions.delete(app);
|
|
928
|
+
removeRow(completion.item);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function clearCompletions() {
|
|
932
|
+
[...completions.keys()].forEach(removeCompletion);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function scheduleCompletion(completion) {
|
|
936
|
+
if (completion.paused.size || completion.timer) return;
|
|
937
|
+
completion.startedAt = Date.now();
|
|
938
|
+
completion.timer = window.setTimeout(() => {
|
|
939
|
+
completion.timer = null;
|
|
940
|
+
removeCompletion(completion.app);
|
|
941
|
+
}, completion.remaining);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function setCompletionPaused(completion, reason, paused) {
|
|
945
|
+
if (paused) {
|
|
946
|
+
if (completion.paused.has(reason)) return;
|
|
947
|
+
if (!completion.paused.size && completion.timer) {
|
|
948
|
+
completion.remaining = Math.max(0,
|
|
949
|
+
completion.remaining - (Date.now() - completion.startedAt));
|
|
950
|
+
window.clearTimeout(completion.timer);
|
|
951
|
+
completion.timer = null;
|
|
952
|
+
}
|
|
953
|
+
completion.paused.add(reason);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
completion.paused.delete(reason);
|
|
957
|
+
if (!completion.paused.size) scheduleCompletion(completion);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function startCompletion(app) {
|
|
961
|
+
if (completions.has(app)) return;
|
|
962
|
+
const item = document.createElement('div');
|
|
963
|
+
item.className = 'vault-auto-scan-row';
|
|
964
|
+
item.dataset.state = 'complete';
|
|
965
|
+
|
|
966
|
+
const close = document.createElement('button');
|
|
967
|
+
close.type = 'button';
|
|
968
|
+
close.className = 'vault-auto-scan-close';
|
|
969
|
+
close.setAttribute('aria-label',
|
|
970
|
+
`Dismiss Disk Saver completion for ${app}`);
|
|
971
|
+
close.title = 'Dismiss';
|
|
972
|
+
close.textContent = '×';
|
|
973
|
+
|
|
974
|
+
const icon = document.createElement('span');
|
|
975
|
+
icon.className = 'vault-auto-scan-icon';
|
|
976
|
+
icon.innerHTML = COMPLETE_ICON;
|
|
977
|
+
|
|
978
|
+
const copy = document.createElement('span');
|
|
979
|
+
copy.className = 'vault-auto-scan-copy';
|
|
980
|
+
const appName = document.createElement('span');
|
|
981
|
+
appName.className = 'vault-auto-scan-app';
|
|
982
|
+
appName.textContent = app;
|
|
983
|
+
appName.title = app;
|
|
984
|
+
const status = document.createElement('span');
|
|
985
|
+
status.className = 'vault-auto-scan-status';
|
|
986
|
+
status.textContent = 'No possible duplicate files found';
|
|
987
|
+
copy.append(appName, status);
|
|
988
|
+
item.append(close, icon, copy);
|
|
989
|
+
|
|
990
|
+
const completion = {
|
|
991
|
+
app,
|
|
992
|
+
item,
|
|
993
|
+
timer: null,
|
|
994
|
+
remaining: COMPLETION_VISIBLE_MS,
|
|
995
|
+
startedAt: 0,
|
|
996
|
+
paused: new Set()
|
|
997
|
+
};
|
|
998
|
+
close.addEventListener('click', () => removeCompletion(app));
|
|
999
|
+
item.addEventListener('mouseenter', () =>
|
|
1000
|
+
setCompletionPaused(completion, 'pointer', true));
|
|
1001
|
+
item.addEventListener('mouseleave', () =>
|
|
1002
|
+
setCompletionPaused(completion, 'pointer', false));
|
|
1003
|
+
item.addEventListener('focusin', () =>
|
|
1004
|
+
setCompletionPaused(completion, 'focus', true));
|
|
1005
|
+
item.addEventListener('focusout', (event) => {
|
|
1006
|
+
if (!item.contains(event.relatedTarget)) {
|
|
1007
|
+
setCompletionPaused(completion, 'focus', false);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
completions.set(app, completion);
|
|
1011
|
+
scheduleCompletion(completion);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
915
1014
|
function automaticSettingsKey(app) {
|
|
916
1015
|
return `pinokio:vault:auto-settings:${encodeURIComponent(app)}`;
|
|
917
1016
|
}
|
|
@@ -968,15 +1067,34 @@
|
|
|
968
1067
|
return opened;
|
|
969
1068
|
}
|
|
970
1069
|
|
|
971
|
-
function render(snapshot) {
|
|
1070
|
+
function render(snapshot, options = {}) {
|
|
972
1071
|
const rows = snapshot && Array.isArray(snapshot.rows)
|
|
973
1072
|
? snapshot.rows
|
|
974
1073
|
: [];
|
|
1074
|
+
const validRows = rows.filter((row) =>
|
|
1075
|
+
row && typeof row.app === 'string' && row.app);
|
|
1076
|
+
const liveApps = new Set(validRows.map((row) => row.app));
|
|
1077
|
+
[...completions.keys()].forEach((app) => {
|
|
1078
|
+
if (liveApps.has(app)) removeCompletion(app);
|
|
1079
|
+
});
|
|
1080
|
+
const manualApps = new Set(snapshot && Array.isArray(snapshot.settings)
|
|
1081
|
+
? snapshot.settings.filter((setting) =>
|
|
1082
|
+
setting && setting.mode === 'manual').map((setting) => setting.app)
|
|
1083
|
+
: []);
|
|
1084
|
+
manualApps.forEach(removeCompletion);
|
|
1085
|
+
|
|
1086
|
+
const completion = snapshot && snapshot.completion;
|
|
1087
|
+
if (options.acceptCompletion && completion &&
|
|
1088
|
+
completion.outcome === 'no_possible_duplicates' &&
|
|
1089
|
+
typeof completion.app === 'string' && completion.app &&
|
|
1090
|
+
visibleCheckingApps.has(completion.app) &&
|
|
1091
|
+
!liveApps.has(completion.app) &&
|
|
1092
|
+
!manualApps.has(completion.app)) {
|
|
1093
|
+
startCompletion(completion.app);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
975
1096
|
const fragment = document.createDocumentFragment();
|
|
976
|
-
|
|
977
|
-
if (!row || typeof row.app !== 'string' || !row.app) {
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
1097
|
+
validRows.forEach((row) => {
|
|
980
1098
|
const item = document.createElement('div');
|
|
981
1099
|
item.className = 'vault-auto-scan-row';
|
|
982
1100
|
item.dataset.state = row.state || 'checking';
|
|
@@ -988,6 +1106,7 @@
|
|
|
988
1106
|
close.title = 'Dismiss';
|
|
989
1107
|
close.textContent = '×';
|
|
990
1108
|
close.addEventListener('click', async () => {
|
|
1109
|
+
if (row.state === 'checking') visibleCheckingApps.delete(row.app);
|
|
991
1110
|
close.disabled = true;
|
|
992
1111
|
try {
|
|
993
1112
|
const result = await requestAction(
|
|
@@ -1000,6 +1119,9 @@
|
|
|
1000
1119
|
removeRow(item);
|
|
1001
1120
|
} catch (error) {
|
|
1002
1121
|
console.warn('[Disk Saver] Automatic check dismissal failed', error);
|
|
1122
|
+
if (row.state === 'checking' && item.isConnected) {
|
|
1123
|
+
visibleCheckingApps.add(row.app);
|
|
1124
|
+
}
|
|
1003
1125
|
close.disabled = false;
|
|
1004
1126
|
}
|
|
1005
1127
|
});
|
|
@@ -1088,8 +1210,16 @@
|
|
|
1088
1210
|
item.append(close, icon, copy, controls);
|
|
1089
1211
|
fragment.appendChild(item);
|
|
1090
1212
|
});
|
|
1213
|
+
completions.forEach((completionRow) => {
|
|
1214
|
+
if (!liveApps.has(completionRow.app)) {
|
|
1215
|
+
fragment.appendChild(completionRow.item);
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1091
1218
|
tray.replaceChildren(fragment);
|
|
1092
1219
|
tray.hidden = tray.childElementCount === 0;
|
|
1220
|
+
visibleCheckingApps = new Set(validRows
|
|
1221
|
+
.filter((row) => row.state === 'checking')
|
|
1222
|
+
.map((row) => row.app));
|
|
1093
1223
|
}
|
|
1094
1224
|
|
|
1095
1225
|
async function loadState() {
|
|
@@ -1119,12 +1249,13 @@
|
|
|
1119
1249
|
'/info/vault/automatic-scans/events');
|
|
1120
1250
|
eventSource.onmessage = (event) => {
|
|
1121
1251
|
try {
|
|
1122
|
-
render(JSON.parse(event.data));
|
|
1252
|
+
render(JSON.parse(event.data), { acceptCompletion: true });
|
|
1123
1253
|
} catch (error) {
|
|
1124
1254
|
console.debug('[Disk Saver] Invalid automatic check state', error);
|
|
1125
1255
|
}
|
|
1126
1256
|
};
|
|
1127
1257
|
eventSource.onerror = () => {
|
|
1258
|
+
clearCompletions();
|
|
1128
1259
|
loadState();
|
|
1129
1260
|
};
|
|
1130
1261
|
}
|
package/server/public/vault.js
CHANGED
|
@@ -2274,8 +2274,8 @@ const renderOverview = () => {
|
|
|
2274
2274
|
: activeScan
|
|
2275
2275
|
? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}`
|
|
2276
2276
|
: `<i class="fa-solid fa-rotate"></i>${esc(idleScanLabel)}`
|
|
2277
|
-
const
|
|
2278
|
-
scanButton.classList.toggle("primary",
|
|
2277
|
+
const firstScan = !last && !activeScan
|
|
2278
|
+
scanButton.classList.toggle("primary", firstScan)
|
|
2279
2279
|
scanButton.disabled = busyElsewhere || state.scanCancelRequested
|
|
2280
2280
|
if (state.automaticReviewRequested && !activeScan &&
|
|
2281
2281
|
!scanButton.disabled) {
|
|
@@ -2296,7 +2296,7 @@ const renderOverview = () => {
|
|
|
2296
2296
|
if (scanSizeMenu) {
|
|
2297
2297
|
const scanSizeTrigger = scanSizeMenu.querySelector("summary")
|
|
2298
2298
|
if (scanSizeTrigger) {
|
|
2299
|
-
scanSizeTrigger.classList.toggle("primary",
|
|
2299
|
+
scanSizeTrigger.classList.toggle("primary", firstScan)
|
|
2300
2300
|
}
|
|
2301
2301
|
scanSizeMenu.hidden = activeScan
|
|
2302
2302
|
if (activeScan) scanSizeMenu.open = false
|
package/server/views/app.ejs
CHANGED
|
@@ -4102,6 +4102,48 @@ body.dark .disk-usage {
|
|
|
4102
4102
|
flex: 0 0 auto;
|
|
4103
4103
|
min-width: 0;
|
|
4104
4104
|
}
|
|
4105
|
+
.appcanvas.vertical > aside .menu-actions #save-space-tab {
|
|
4106
|
+
width: 100%;
|
|
4107
|
+
max-width: none;
|
|
4108
|
+
box-sizing: border-box;
|
|
4109
|
+
}
|
|
4110
|
+
.app-vault-mode {
|
|
4111
|
+
flex: 0 0 auto;
|
|
4112
|
+
margin-left: auto;
|
|
4113
|
+
color: rgba(71, 85, 105, 0.82);
|
|
4114
|
+
font-size: 12px;
|
|
4115
|
+
font-weight: 500;
|
|
4116
|
+
letter-spacing: 0;
|
|
4117
|
+
line-height: 1.2;
|
|
4118
|
+
text-align: right;
|
|
4119
|
+
white-space: nowrap;
|
|
4120
|
+
}
|
|
4121
|
+
.app-vault-mode[data-mode="automatic"] {
|
|
4122
|
+
color: #b45309;
|
|
4123
|
+
}
|
|
4124
|
+
body.dark .app-vault-mode {
|
|
4125
|
+
color: rgba(203, 213, 225, 0.74);
|
|
4126
|
+
}
|
|
4127
|
+
body.dark .app-vault-mode[data-mode="automatic"] {
|
|
4128
|
+
color: #e0a54b;
|
|
4129
|
+
}
|
|
4130
|
+
.app-vault-mode-chevron,
|
|
4131
|
+
.app-autolaunch-chevron {
|
|
4132
|
+
display: inline-flex;
|
|
4133
|
+
align-items: center;
|
|
4134
|
+
justify-content: center;
|
|
4135
|
+
width: 10px;
|
|
4136
|
+
flex: 0 0 10px;
|
|
4137
|
+
margin-right: 0;
|
|
4138
|
+
padding: 0;
|
|
4139
|
+
color: var(--pinokio-sidebar-action-muted);
|
|
4140
|
+
font-size: 10px;
|
|
4141
|
+
line-height: 1;
|
|
4142
|
+
box-sizing: border-box;
|
|
4143
|
+
}
|
|
4144
|
+
.appcanvas > aside .menu-actions .header-item .tab .app-vault-mode-chevron {
|
|
4145
|
+
margin-right: 0;
|
|
4146
|
+
}
|
|
4105
4147
|
.app-autolaunch-row {
|
|
4106
4148
|
appearance: none;
|
|
4107
4149
|
display: flex;
|
|
@@ -4171,19 +4213,14 @@ body.dark .disk-usage {
|
|
|
4171
4213
|
.app-autolaunch-status {
|
|
4172
4214
|
flex: 0 0 auto;
|
|
4173
4215
|
color: var(--pinokio-sidebar-action-muted);
|
|
4174
|
-
font-size:
|
|
4175
|
-
font-weight:
|
|
4176
|
-
letter-spacing: 0
|
|
4216
|
+
font-size: 12px;
|
|
4217
|
+
font-weight: 500;
|
|
4218
|
+
letter-spacing: 0;
|
|
4177
4219
|
white-space: nowrap;
|
|
4178
4220
|
}
|
|
4179
4221
|
.app-autolaunch-row[data-enabled="true"] .app-autolaunch-status {
|
|
4180
4222
|
color: rgba(22, 101, 52, 0.95);
|
|
4181
4223
|
}
|
|
4182
|
-
.app-autolaunch-chevron {
|
|
4183
|
-
flex: 0 0 auto;
|
|
4184
|
-
color: var(--pinokio-sidebar-action-muted);
|
|
4185
|
-
font-size: 10px;
|
|
4186
|
-
}
|
|
4187
4224
|
body.dark .app-autolaunch-row {
|
|
4188
4225
|
background: transparent;
|
|
4189
4226
|
color: var(--pinokio-sidebar-tab-muted);
|
|
@@ -4193,6 +4230,7 @@ body.dark .app-autolaunch.open .app-autolaunch-row {
|
|
|
4193
4230
|
background: var(--pinokio-sidebar-tab-hover);
|
|
4194
4231
|
color: var(--pinokio-sidebar-tab-active-color);
|
|
4195
4232
|
}
|
|
4233
|
+
body.dark .app-vault-mode-chevron,
|
|
4196
4234
|
body.dark .app-autolaunch-status,
|
|
4197
4235
|
body.dark .app-autolaunch-chevron {
|
|
4198
4236
|
color: var(--pinokio-sidebar-action-muted);
|
|
@@ -8163,6 +8201,7 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8163
8201
|
<script src="/tippy-bundle.umd.min.js"></script>
|
|
8164
8202
|
<script src="/tab-link-popover.js"></script>
|
|
8165
8203
|
<script src="/browser-popout-surface.js"></script>
|
|
8204
|
+
<script src="/app-vault-mode.js" defer></script>
|
|
8166
8205
|
<script>
|
|
8167
8206
|
(function() {
|
|
8168
8207
|
try {
|
|
@@ -8461,11 +8500,16 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8461
8500
|
</div>
|
|
8462
8501
|
</button>
|
|
8463
8502
|
<% if (typeof vault_enabled !== 'undefined' && vault_enabled) { %>
|
|
8464
|
-
|
|
8503
|
+
<% const vaultAutomaticMode = typeof vault_automatic_mode === 'string' && vault_automatic_mode === 'manual' ? 'manual' : 'automatic' %>
|
|
8504
|
+
<a id='save-space-tab' data-mode="refresh" target="app-vault" href="/vault/app/<%=encodeURIComponent(name)%>" class="btn header-item frame-link" data-index="vault" data-static="retain" data-tab-link-popover="false" aria-label="Disk Saver — <%=vaultAutomaticMode === 'automatic' ? 'Automatic' : 'Manual'%> checking">
|
|
8465
8505
|
<div class='tab'>
|
|
8466
8506
|
<i class="fa-solid fa-hard-drive menu-action-leading-icon"></i>
|
|
8467
8507
|
<div class='display'>Disk Saver</div>
|
|
8468
8508
|
<div class='flexible'></div>
|
|
8509
|
+
<span class="app-vault-mode" data-app-vault-mode data-app="<%=name%>" data-mode="<%=vaultAutomaticMode%>" aria-hidden="true">
|
|
8510
|
+
<span data-app-vault-mode-label><%=vaultAutomaticMode === 'automatic' ? 'Auto' : 'Manual'%></span>
|
|
8511
|
+
</span>
|
|
8512
|
+
<i class="fa-solid fa-angle-down app-vault-mode-chevron" aria-hidden="true"></i>
|
|
8469
8513
|
</div>
|
|
8470
8514
|
</a>
|
|
8471
8515
|
<% } %>
|
|
@@ -8474,7 +8518,7 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8474
8518
|
<button type="button" class="app-autolaunch-row" data-app-autolaunch-button data-enabled="<%= autolaunch_app.autolaunch_enabled ? 'true' : 'false' %>" aria-haspopup="dialog" aria-expanded="false">
|
|
8475
8519
|
<span class="app-autolaunch-label"><i class="fa-solid fa-power-off"></i><span>Autolaunch</span></span>
|
|
8476
8520
|
<span class="app-autolaunch-spacer" aria-hidden="true"></span>
|
|
8477
|
-
<span class="app-autolaunch-status" data-app-autolaunch-status><%= autolaunch_app.autolaunch_enabled ? '
|
|
8521
|
+
<span class="app-autolaunch-status" data-app-autolaunch-status><%= autolaunch_app.autolaunch_enabled ? 'On' : 'Off' %></span>
|
|
8478
8522
|
<i class="fa-solid fa-angle-down app-autolaunch-chevron" aria-hidden="true"></i>
|
|
8479
8523
|
</button>
|
|
8480
8524
|
<div class="app-autolaunch-modal hidden" data-app-autolaunch-modal role="dialog" aria-modal="true" aria-label="Autolaunch">
|
package/server/views/layout.ejs
CHANGED
|
@@ -333,7 +333,8 @@
|
|
|
333
333
|
opacity: 0.55;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
.vault-auto-scan-row[data-state="result"]
|
|
336
|
+
.vault-auto-scan-row[data-state="result"],
|
|
337
|
+
.vault-auto-scan-row[data-state="complete"] {
|
|
337
338
|
grid-template-rows: 16px 30px;
|
|
338
339
|
min-height: 66px;
|
|
339
340
|
padding-top: 10px;
|
|
@@ -342,14 +343,25 @@
|
|
|
342
343
|
|
|
343
344
|
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-icon,
|
|
344
345
|
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
|
|
345
|
-
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-controls
|
|
346
|
+
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-controls,
|
|
347
|
+
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-icon,
|
|
348
|
+
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
|
|
346
349
|
grid-row: 1 / 3;
|
|
347
350
|
}
|
|
348
351
|
|
|
349
|
-
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy
|
|
352
|
+
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
|
|
353
|
+
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
|
|
350
354
|
grid-template-rows: 16px 30px;
|
|
351
355
|
}
|
|
352
356
|
|
|
357
|
+
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
|
|
358
|
+
grid-column: 2 / 4;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-icon {
|
|
362
|
+
color: var(--vault-notice-accent);
|
|
363
|
+
}
|
|
364
|
+
|
|
353
365
|
@keyframes vault-notice-enter {
|
|
354
366
|
from { opacity: 0; transform: translateY(6px); }
|
|
355
367
|
to { opacity: 1; transform: translateY(0); }
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
}
|
|
170
170
|
const renderStatus = () => {
|
|
171
171
|
const enabled = !!(state && state.autolaunch_enabled)
|
|
172
|
-
status.textContent = enabled ? "
|
|
172
|
+
status.textContent = enabled ? "On" : "Off"
|
|
173
173
|
button.dataset.enabled = enabled ? "true" : "false"
|
|
174
174
|
button.setAttribute("aria-label", `Autolaunch. Start with Pinokio ${enabled ? "On" : "Off"}`)
|
|
175
175
|
switchButton.setAttribute("aria-checked", enabled ? "true" : "false")
|
|
@@ -122,7 +122,7 @@ function appAutolaunchMarkup(initialApp) {
|
|
|
122
122
|
<div class="app-autolaunch" data-app-autolaunch data-app-id="target">
|
|
123
123
|
<button type="button" class="app-autolaunch-row" data-app-autolaunch-button data-enabled="${initialApp.autolaunch_enabled ? "true" : "false"}" aria-haspopup="dialog" aria-expanded="false">
|
|
124
124
|
<span class="app-autolaunch-label">Autolaunch</span>
|
|
125
|
-
<span class="app-autolaunch-status" data-app-autolaunch-status>${initialApp.autolaunch_enabled ? "
|
|
125
|
+
<span class="app-autolaunch-status" data-app-autolaunch-status>${initialApp.autolaunch_enabled ? "On" : "Off"}</span>
|
|
126
126
|
</button>
|
|
127
127
|
<div class="app-autolaunch-modal hidden" data-app-autolaunch-modal role="dialog" aria-modal="true" aria-label="Autolaunch">
|
|
128
128
|
<button type="button" class="app-autolaunch-switch" role="switch" aria-checked="${initialApp.autolaunch_enabled ? "true" : "false"}" data-app-autolaunch-switch aria-label="Start with Pinokio">
|
|
@@ -478,7 +478,7 @@ browserTest("browser: selecting a launch script from empty state persists and st
|
|
|
478
478
|
enabled: false
|
|
479
479
|
})
|
|
480
480
|
assert.equal(await page.isChecked('input[name="app-autolaunch-script"][value="start.js"]'), true)
|
|
481
|
-
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "
|
|
481
|
+
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "Off")
|
|
482
482
|
})
|
|
483
483
|
})
|
|
484
484
|
|
|
@@ -532,7 +532,7 @@ browserTest("browser: startup toggle from empty selection saves the single eligi
|
|
|
532
532
|
})
|
|
533
533
|
assert.equal(state.launchRequirementsGets, 0)
|
|
534
534
|
assert.equal(await page.isChecked('input[name="app-autolaunch-script"][value="start.js"]'), true)
|
|
535
|
-
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "
|
|
535
|
+
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "On")
|
|
536
536
|
})
|
|
537
537
|
})
|
|
538
538
|
|
|
@@ -570,7 +570,7 @@ browserTest("browser: startup toggle from empty selection warns when no eligible
|
|
|
570
570
|
})
|
|
571
571
|
|
|
572
572
|
assert.equal(state.autolaunchPosts.length, 0, scenario.name)
|
|
573
|
-
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "
|
|
573
|
+
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "Off")
|
|
574
574
|
})
|
|
575
575
|
}
|
|
576
576
|
})
|
|
@@ -227,6 +227,234 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
|
|
|
227
227
|
dom.window.close()
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
+
test("an empty automatic check briefly confirms completion", async () => {
|
|
231
|
+
const template = await fs.promises.readFile(
|
|
232
|
+
path.join(root, "server", "views", "layout.ejs"), "utf8")
|
|
233
|
+
const script = await fs.promises.readFile(
|
|
234
|
+
path.join(root, "server", "public", "layout.js"), "utf8")
|
|
235
|
+
const html = ejs.render(template, {
|
|
236
|
+
theme: "light",
|
|
237
|
+
agent: "web",
|
|
238
|
+
initialPath: "/v/ComfyUI",
|
|
239
|
+
defaultPath: "/home",
|
|
240
|
+
sessionId: null,
|
|
241
|
+
vaultEnabled: true
|
|
242
|
+
})
|
|
243
|
+
const dom = new JSDOM(html, {
|
|
244
|
+
runScripts: "outside-only",
|
|
245
|
+
pretendToBeVisual: true,
|
|
246
|
+
url: "http://localhost/"
|
|
247
|
+
})
|
|
248
|
+
const eventSources = []
|
|
249
|
+
const requests = []
|
|
250
|
+
const completionTimers = []
|
|
251
|
+
const nativeSetTimeout = dom.window.setTimeout.bind(dom.window)
|
|
252
|
+
const nativeClearTimeout = dom.window.clearTimeout.bind(dom.window)
|
|
253
|
+
dom.window.setTimeout = (callback, delay, ...args) => {
|
|
254
|
+
if (delay <= 4000 && delay > 3500) {
|
|
255
|
+
const timer = {
|
|
256
|
+
callback,
|
|
257
|
+
delay,
|
|
258
|
+
cleared: false,
|
|
259
|
+
id: 10000 + completionTimers.length
|
|
260
|
+
}
|
|
261
|
+
completionTimers.push(timer)
|
|
262
|
+
return timer.id
|
|
263
|
+
}
|
|
264
|
+
return nativeSetTimeout(callback, delay, ...args)
|
|
265
|
+
}
|
|
266
|
+
dom.window.clearTimeout = (id) => {
|
|
267
|
+
const timer = completionTimers.find((candidate) => candidate.id === id)
|
|
268
|
+
if (timer) {
|
|
269
|
+
timer.cleared = true
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
nativeClearTimeout(id)
|
|
273
|
+
}
|
|
274
|
+
dom.window.EventSource = class EventSource {
|
|
275
|
+
constructor(url) {
|
|
276
|
+
this.url = url
|
|
277
|
+
eventSources.push(this)
|
|
278
|
+
}
|
|
279
|
+
close() {}
|
|
280
|
+
}
|
|
281
|
+
dom.window.fetch = async (url, options = {}) => {
|
|
282
|
+
requests.push({ url, options })
|
|
283
|
+
if (url === "/info/vault/automatic-scans") {
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
status: 200,
|
|
287
|
+
json: async () => ({
|
|
288
|
+
enabled: true,
|
|
289
|
+
rows: [],
|
|
290
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (url === "/vault/action") {
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
status: 200,
|
|
298
|
+
json: async () => ({ dismissed: true, app: "ComfyUI" })
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`Unexpected request: ${url}`)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
dom.window.eval(script)
|
|
305
|
+
await waitFor(() => eventSources.length === 1)
|
|
306
|
+
const send = (payload) => eventSources[0].onmessage({
|
|
307
|
+
data: JSON.stringify(payload)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
send({
|
|
311
|
+
enabled: true,
|
|
312
|
+
rows: [],
|
|
313
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }],
|
|
314
|
+
completion: {
|
|
315
|
+
app: "ComfyUI",
|
|
316
|
+
outcome: "no_possible_duplicates"
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
const tray = dom.window.document.getElementById("vault-auto-scan-tray")
|
|
320
|
+
assert.equal(tray.hidden, true,
|
|
321
|
+
"a completion event cannot appear without a visible checking row")
|
|
322
|
+
|
|
323
|
+
send({
|
|
324
|
+
enabled: true,
|
|
325
|
+
rows: [
|
|
326
|
+
{
|
|
327
|
+
app: "ComfyUI",
|
|
328
|
+
state: "checking",
|
|
329
|
+
notice_id: "checking:1:"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
app: "OtherApp",
|
|
333
|
+
state: "paused",
|
|
334
|
+
notice_id: "paused:1:"
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
settings: [
|
|
338
|
+
{ app: "ComfyUI", mode: "automatic" },
|
|
339
|
+
{ app: "OtherApp", mode: "manual" }
|
|
340
|
+
]
|
|
341
|
+
})
|
|
342
|
+
send({
|
|
343
|
+
enabled: true,
|
|
344
|
+
rows: [{
|
|
345
|
+
app: "OtherApp",
|
|
346
|
+
state: "paused",
|
|
347
|
+
notice_id: "paused:1:"
|
|
348
|
+
}],
|
|
349
|
+
settings: [
|
|
350
|
+
{ app: "ComfyUI", mode: "automatic" },
|
|
351
|
+
{ app: "OtherApp", mode: "manual" }
|
|
352
|
+
],
|
|
353
|
+
completion: {
|
|
354
|
+
app: "ComfyUI",
|
|
355
|
+
outcome: "no_possible_duplicates"
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const completed = tray.querySelector(
|
|
360
|
+
'.vault-auto-scan-row[data-state="complete"]')
|
|
361
|
+
assert.ok(completed)
|
|
362
|
+
assert.equal(completed.querySelector(
|
|
363
|
+
".vault-auto-scan-app").textContent, "ComfyUI")
|
|
364
|
+
assert.equal(completed.querySelector(
|
|
365
|
+
".vault-auto-scan-status").textContent,
|
|
366
|
+
"No possible duplicate files found")
|
|
367
|
+
assert.ok(completed.querySelector(".vault-auto-scan-icon svg"))
|
|
368
|
+
assert.equal(completed.querySelector(".vault-auto-scan-settings"), null)
|
|
369
|
+
assert.equal(completed.querySelector(".vault-auto-scan-action"), null)
|
|
370
|
+
assert.equal(completionTimers[0].delay, 4000)
|
|
371
|
+
|
|
372
|
+
tray.querySelector(
|
|
373
|
+
'.vault-auto-scan-row[data-state="paused"] .vault-auto-scan-action').click()
|
|
374
|
+
await waitFor(() => requests.some((request) =>
|
|
375
|
+
request.url === "/info/vault/automatic-scans"))
|
|
376
|
+
await new Promise((resolve) => nativeSetTimeout(resolve, 0))
|
|
377
|
+
assert.equal(completed.isConnected, true,
|
|
378
|
+
"refreshing durable tray state preserves an active completion")
|
|
379
|
+
assert.equal(completionTimers.length, 1,
|
|
380
|
+
"a durable state refresh does not restart the completion timer")
|
|
381
|
+
|
|
382
|
+
completed.dispatchEvent(new dom.window.MouseEvent("mouseenter"))
|
|
383
|
+
assert.equal(completionTimers[0].cleared, true)
|
|
384
|
+
completed.dispatchEvent(new dom.window.MouseEvent("mouseleave"))
|
|
385
|
+
assert.equal(completionTimers.length, 2)
|
|
386
|
+
assert.ok(completionTimers[1].delay <= 4000)
|
|
387
|
+
|
|
388
|
+
const closeButton = completed.querySelector(".vault-auto-scan-close")
|
|
389
|
+
closeButton.dispatchEvent(new dom.window.FocusEvent("focusin", {
|
|
390
|
+
bubbles: true
|
|
391
|
+
}))
|
|
392
|
+
assert.equal(completionTimers[1].cleared, true)
|
|
393
|
+
closeButton.dispatchEvent(new dom.window.FocusEvent("focusout", {
|
|
394
|
+
bubbles: true,
|
|
395
|
+
relatedTarget: null
|
|
396
|
+
}))
|
|
397
|
+
assert.equal(completionTimers.length, 3)
|
|
398
|
+
completionTimers[2].callback()
|
|
399
|
+
assert.equal(tray.hidden, true)
|
|
400
|
+
|
|
401
|
+
send({
|
|
402
|
+
enabled: true,
|
|
403
|
+
rows: [{
|
|
404
|
+
app: "ComfyUI",
|
|
405
|
+
state: "checking",
|
|
406
|
+
notice_id: "checking:2:"
|
|
407
|
+
}],
|
|
408
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
409
|
+
})
|
|
410
|
+
send({
|
|
411
|
+
enabled: true,
|
|
412
|
+
rows: [],
|
|
413
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }],
|
|
414
|
+
completion: {
|
|
415
|
+
app: "ComfyUI",
|
|
416
|
+
outcome: "no_possible_duplicates"
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
assert.equal(tray.hidden, false)
|
|
420
|
+
eventSources[0].onerror()
|
|
421
|
+
await new Promise((resolve) => nativeSetTimeout(resolve, 0))
|
|
422
|
+
assert.equal(tray.hidden, true,
|
|
423
|
+
"a reconnect drops presentation-only completion state")
|
|
424
|
+
|
|
425
|
+
send({
|
|
426
|
+
enabled: true,
|
|
427
|
+
rows: [{
|
|
428
|
+
app: "ComfyUI",
|
|
429
|
+
state: "checking",
|
|
430
|
+
notice_id: "checking:3:"
|
|
431
|
+
}],
|
|
432
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
433
|
+
})
|
|
434
|
+
tray.querySelector(".vault-auto-scan-close").click()
|
|
435
|
+
send({
|
|
436
|
+
enabled: true,
|
|
437
|
+
rows: [],
|
|
438
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }],
|
|
439
|
+
completion: {
|
|
440
|
+
app: "ComfyUI",
|
|
441
|
+
outcome: "no_possible_duplicates"
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
await waitFor(() => tray.hidden)
|
|
445
|
+
assert.equal(tray.querySelector(
|
|
446
|
+
'.vault-auto-scan-row[data-state="complete"]'), null,
|
|
447
|
+
"a closed checking row cannot be replaced by completion")
|
|
448
|
+
assert.ok(requests.some((request) => {
|
|
449
|
+
if (request.url !== "/vault/action" || !request.options.body) return false
|
|
450
|
+
const payload = JSON.parse(request.options.body)
|
|
451
|
+
return payload.action === "automatic_dismiss" &&
|
|
452
|
+
payload.notice_id === "checking:3:"
|
|
453
|
+
}))
|
|
454
|
+
|
|
455
|
+
dom.window.close()
|
|
456
|
+
})
|
|
457
|
+
|
|
230
458
|
test("the shared layout does not initialize automatic notices when Vault is disabled", async () => {
|
|
231
459
|
const template = await fs.promises.readFile(
|
|
232
460
|
path.join(root, "server", "views", "layout.ejs"), "utf8")
|
|
@@ -290,3 +518,80 @@ test("the app workspace focuses Scan this app without starting it after Review",
|
|
|
290
518
|
assert.doesNotMatch(source,
|
|
291
519
|
/automaticReviewRequested[\s\S]{0,200}(post\(|btn-scan\.click)/)
|
|
292
520
|
})
|
|
521
|
+
|
|
522
|
+
test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () => {
|
|
523
|
+
const template = await fs.promises.readFile(
|
|
524
|
+
path.join(root, "server", "views", "app.ejs"), "utf8")
|
|
525
|
+
const server = await fs.promises.readFile(
|
|
526
|
+
path.join(root, "server", "index.js"), "utf8")
|
|
527
|
+
const script = await fs.promises.readFile(
|
|
528
|
+
path.join(root, "server", "public", "app-vault-mode.js"), "utf8")
|
|
529
|
+
const dom = new JSDOM(`<a id="save-space-tab">
|
|
530
|
+
<span data-app-vault-mode data-app="ComfyUI" data-mode="automatic">
|
|
531
|
+
<span data-app-vault-mode-label>Auto</span>
|
|
532
|
+
</span>
|
|
533
|
+
</a>`, {
|
|
534
|
+
runScripts: "outside-only",
|
|
535
|
+
url: "http://localhost/v/ComfyUI"
|
|
536
|
+
})
|
|
537
|
+
const eventSources = []
|
|
538
|
+
dom.window.EventSource = class EventSource {
|
|
539
|
+
constructor(url) {
|
|
540
|
+
this.url = url
|
|
541
|
+
eventSources.push(this)
|
|
542
|
+
}
|
|
543
|
+
close() {}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
dom.window.eval(script)
|
|
547
|
+
const status = dom.window.document.querySelector("[data-app-vault-mode]")
|
|
548
|
+
const label = status.querySelector("[data-app-vault-mode-label]")
|
|
549
|
+
assert.equal(eventSources.length, 1)
|
|
550
|
+
assert.equal(eventSources[0].url,
|
|
551
|
+
"/info/vault/automatic-scans/events")
|
|
552
|
+
assert.equal(status.dataset.mode, "automatic")
|
|
553
|
+
assert.equal(status.hidden, false)
|
|
554
|
+
assert.equal(label.textContent, "Auto")
|
|
555
|
+
assert.equal(dom.window.document.getElementById("save-space-tab")
|
|
556
|
+
.getAttribute("aria-label"), "Disk Saver — Automatic checking")
|
|
557
|
+
|
|
558
|
+
eventSources[0].onmessage({
|
|
559
|
+
data: JSON.stringify({
|
|
560
|
+
settings: [{ app: "ComfyUI", mode: "manual" }]
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
assert.equal(status.dataset.mode, "manual")
|
|
564
|
+
assert.equal(status.hidden, false)
|
|
565
|
+
assert.equal(label.textContent, "Manual")
|
|
566
|
+
assert.equal(dom.window.document.getElementById("save-space-tab")
|
|
567
|
+
.getAttribute("aria-label"), "Disk Saver — Manual checking")
|
|
568
|
+
|
|
569
|
+
eventSources[0].onmessage({ data: JSON.stringify({ settings: [] }) })
|
|
570
|
+
assert.equal(status.dataset.mode, "automatic")
|
|
571
|
+
assert.equal(status.hidden, false)
|
|
572
|
+
assert.equal(label.textContent, "Auto")
|
|
573
|
+
assert.match(template, /data-app-vault-mode/)
|
|
574
|
+
assert.match(template, /app-vault-mode\.js/)
|
|
575
|
+
assert.match(template,
|
|
576
|
+
/#save-space-tab\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*none;/s)
|
|
577
|
+
assert.match(template,
|
|
578
|
+
/\.app-vault-mode\s*\{[^}]*margin-left:\s*auto;[^}]*font-size:\s*12px;[^}]*font-weight:\s*500;/s)
|
|
579
|
+
assert.match(template,
|
|
580
|
+
/\.app-vault-mode-chevron,\s*\.app-autolaunch-chevron\s*\{[^}]*width:\s*10px;[^}]*flex:\s*0 0 10px;/s)
|
|
581
|
+
assert.doesNotMatch(template,
|
|
582
|
+
/\.app-vault-mode\s*\{[^}]*(background|border-radius|padding):/s)
|
|
583
|
+
assert.match(template,
|
|
584
|
+
/vaultAutomaticMode === 'automatic' \? 'Auto' : 'Manual'/)
|
|
585
|
+
assert.match(template,
|
|
586
|
+
/fa-solid fa-angle-down app-vault-mode-chevron/)
|
|
587
|
+
assert.match(template,
|
|
588
|
+
/autolaunch_app\.autolaunch_enabled \? 'On' : 'Off'/)
|
|
589
|
+
assert.match(template,
|
|
590
|
+
/\.app-autolaunch-status\s*\{[^}]*font-size:\s*12px;[^}]*font-weight:\s*500;[^}]*letter-spacing:\s*0;/s)
|
|
591
|
+
assert.doesNotMatch(template, /data-app-vault-mode[^>]*hidden/)
|
|
592
|
+
assert.doesNotMatch(template, /app-vault-mode-dot/)
|
|
593
|
+
assert.match(server,
|
|
594
|
+
/result\.vault_automatic_mode = setting && setting\.mode === "manual"/)
|
|
595
|
+
|
|
596
|
+
dom.window.close()
|
|
597
|
+
})
|
|
@@ -409,12 +409,29 @@ describe("automatic app checks", () => {
|
|
|
409
409
|
await handle.close()
|
|
410
410
|
}
|
|
411
411
|
const vault = await makeVault(home)
|
|
412
|
+
const broadcasts = []
|
|
413
|
+
const unsubscribe = vault.automaticScans.subscribe((snapshot) => {
|
|
414
|
+
broadcasts.push(snapshot)
|
|
415
|
+
})
|
|
412
416
|
|
|
413
417
|
vault.automaticScans.queueApp("threshold-app")
|
|
414
418
|
await waitFor(() => !vault.automaticScans.active &&
|
|
415
419
|
!vault.automaticScans.entries.has("threshold-app"))
|
|
416
420
|
|
|
417
421
|
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
422
|
+
assert.deepEqual(broadcasts.find((snapshot) => snapshot.completion)
|
|
423
|
+
?.completion, {
|
|
424
|
+
app: "threshold-app",
|
|
425
|
+
outcome: "no_possible_duplicates"
|
|
426
|
+
})
|
|
427
|
+
assert.equal("completion" in vault.automaticScans.snapshot(), false)
|
|
428
|
+
let restoredSnapshot = null
|
|
429
|
+
const unsubscribeRestored = vault.automaticScans.subscribe((snapshot) => {
|
|
430
|
+
restoredSnapshot = snapshot
|
|
431
|
+
})
|
|
432
|
+
assert.equal("completion" in restoredSnapshot, false)
|
|
433
|
+
unsubscribeRestored()
|
|
434
|
+
unsubscribe()
|
|
418
435
|
assert.equal(await vault.registry.scanFor("app:threshold-app"), null)
|
|
419
436
|
await close(vault)
|
|
420
437
|
})
|
package/test/vault-ui.test.js
CHANGED
|
@@ -2184,6 +2184,26 @@ describe("Save Space interface", () => {
|
|
|
2184
2184
|
dom.window.close()
|
|
2185
2185
|
})
|
|
2186
2186
|
|
|
2187
|
+
test("a fresh app workspace makes Scan this app primary", async () => {
|
|
2188
|
+
const status = fixture([], {
|
|
2189
|
+
last_scan: null,
|
|
2190
|
+
bytes_without_sharing: 0,
|
|
2191
|
+
bytes_on_disk: 0,
|
|
2192
|
+
saved_by_sharing: 0,
|
|
2193
|
+
effective_bytes: 0
|
|
2194
|
+
})
|
|
2195
|
+
const { dom } = await makePage(status, {
|
|
2196
|
+
appMode: true,
|
|
2197
|
+
scopeId: "app:app"
|
|
2198
|
+
})
|
|
2199
|
+
const scanButton = dom.window.document.getElementById("btn-scan")
|
|
2200
|
+
|
|
2201
|
+
assert.match(scanButton.textContent, /Scan this app/)
|
|
2202
|
+
assert.equal(scanButton.classList.contains("primary"), true)
|
|
2203
|
+
|
|
2204
|
+
dom.window.close()
|
|
2205
|
+
})
|
|
2206
|
+
|
|
2187
2207
|
test("app mode uses full-width results without redundant locations", async () => {
|
|
2188
2208
|
const status = fixture([item()])
|
|
2189
2209
|
const { dom, requests } = await makePage(status, {
|