pinokiod 8.0.61 → 8.0.63
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/package.json +1 -1
- package/server/public/layout.js +300 -254
- package/server/public/vault.js +0 -14
- package/server/views/app.ejs +0 -22
- package/server/views/layout.ejs +90 -79
- package/test/vault-automatic-scan-ui.test.js +177 -22
package/package.json
CHANGED
package/server/public/layout.js
CHANGED
|
@@ -241,12 +241,7 @@
|
|
|
241
241
|
iframe.addEventListener('focus', markActive);
|
|
242
242
|
iframe.addEventListener('pointerdown', markActive);
|
|
243
243
|
|
|
244
|
-
entry = {
|
|
245
|
-
container,
|
|
246
|
-
iframe,
|
|
247
|
-
updateNodeLocation,
|
|
248
|
-
vaultAutoSettingsReady: false,
|
|
249
|
-
};
|
|
244
|
+
entry = { container, iframe, updateNodeLocation };
|
|
250
245
|
leafElements.set(node.id, entry);
|
|
251
246
|
return entry;
|
|
252
247
|
}
|
|
@@ -641,7 +636,6 @@
|
|
|
641
636
|
leaf.src = state.defaultPath;
|
|
642
637
|
const entry = leafElements.get(leaf.id);
|
|
643
638
|
if (entry) {
|
|
644
|
-
entry.vaultAutoSettingsReady = false;
|
|
645
639
|
entry.iframe.src = leaf.src;
|
|
646
640
|
}
|
|
647
641
|
cleanupSessionIfSingleLeaf();
|
|
@@ -688,7 +682,6 @@
|
|
|
688
682
|
return false;
|
|
689
683
|
}
|
|
690
684
|
node.src = normalized;
|
|
691
|
-
entry.vaultAutoSettingsReady = false;
|
|
692
685
|
entry.iframe.src = normalized;
|
|
693
686
|
state.activeLeafId = frameId;
|
|
694
687
|
saveStateToStorage();
|
|
@@ -699,16 +692,6 @@
|
|
|
699
692
|
if (!event || !event.data || typeof event.data !== 'object') {
|
|
700
693
|
return;
|
|
701
694
|
}
|
|
702
|
-
if (event.data.e === 'vault-auto-settings-ready' &&
|
|
703
|
-
event.origin === window.location.origin) {
|
|
704
|
-
for (const entry of leafElements.values()) {
|
|
705
|
-
if (entry.iframe && entry.iframe.contentWindow === event.source) {
|
|
706
|
-
entry.vaultAutoSettingsReady = true;
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
695
|
if (event.data.e === 'layout-state-request') {
|
|
713
696
|
let frameEntry = null;
|
|
714
697
|
let frameId = null;
|
|
@@ -788,7 +771,6 @@
|
|
|
788
771
|
if (node.type === 'leaf') {
|
|
789
772
|
const entry = ensureLeafElement(node);
|
|
790
773
|
if (entry && entry.iframe.src !== node.src) {
|
|
791
|
-
entry.vaultAutoSettingsReady = false;
|
|
792
774
|
entry.iframe.src = node.src;
|
|
793
775
|
}
|
|
794
776
|
}
|
|
@@ -871,24 +853,27 @@
|
|
|
871
853
|
<circle cx="10" cy="10" r="7.5"></circle>
|
|
872
854
|
<path d="m6.75 10.1 2.1 2.1 4.6-4.65"></path>
|
|
873
855
|
</svg>`;
|
|
856
|
+
const MIN_CHECKING_VISIBLE_MS = 500;
|
|
874
857
|
const COMPLETION_VISIBLE_MS = 4000;
|
|
875
858
|
|
|
876
859
|
let eventSource = null;
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (
|
|
882
|
-
|
|
860
|
+
const cards = new Map();
|
|
861
|
+
|
|
862
|
+
function statusText(state) {
|
|
863
|
+
if (state === 'paused') return 'Automatic checks are paused';
|
|
864
|
+
if (state === 'result') return 'Possible duplicate files found';
|
|
865
|
+
if (state === 'complete') return 'No possible duplicate files found';
|
|
866
|
+
if (state === 'checking-again') {
|
|
867
|
+
return 'Checking again for possible duplicate files...';
|
|
883
868
|
}
|
|
884
|
-
return 'Checking for possible duplicate files
|
|
869
|
+
return 'Checking for possible duplicate files...';
|
|
885
870
|
}
|
|
886
871
|
|
|
887
|
-
function actionFor(
|
|
888
|
-
if (
|
|
872
|
+
function actionFor(state) {
|
|
873
|
+
if (state === 'paused') {
|
|
889
874
|
return { label: 'Resume', action: 'automatic_resume' };
|
|
890
875
|
}
|
|
891
|
-
if (
|
|
876
|
+
if (state === 'result') {
|
|
892
877
|
return { label: 'Review', action: 'automatic_review' };
|
|
893
878
|
}
|
|
894
879
|
return { label: 'Pause', action: 'automatic_pause' };
|
|
@@ -915,33 +900,42 @@
|
|
|
915
900
|
return result || {};
|
|
916
901
|
}
|
|
917
902
|
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
tray.hidden = tray.childElementCount === 0;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function removeCompletion(app) {
|
|
924
|
-
const completion = completions.get(app);
|
|
903
|
+
function stopCompletion(card) {
|
|
904
|
+
const completion = card && card.completion;
|
|
925
905
|
if (!completion) return;
|
|
926
906
|
if (completion.timer) window.clearTimeout(completion.timer);
|
|
927
|
-
|
|
928
|
-
removeRow(completion.item);
|
|
907
|
+
card.completion = null;
|
|
929
908
|
}
|
|
930
909
|
|
|
931
|
-
function
|
|
932
|
-
|
|
910
|
+
function removeCard(app) {
|
|
911
|
+
const card = cards.get(app);
|
|
912
|
+
if (card) {
|
|
913
|
+
stopCompletion(card);
|
|
914
|
+
card.item.remove();
|
|
915
|
+
}
|
|
916
|
+
cards.delete(app);
|
|
917
|
+
tray.hidden = cards.size === 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function resetCards() {
|
|
921
|
+
[...cards.keys()].forEach(removeCard);
|
|
922
|
+
tray.replaceChildren();
|
|
923
|
+
tray.hidden = true;
|
|
933
924
|
}
|
|
934
925
|
|
|
935
926
|
function scheduleCompletion(completion) {
|
|
936
|
-
if (completion.paused.size || completion.timer)
|
|
927
|
+
if (!completion.revealed || completion.paused.size || completion.timer) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
937
930
|
completion.startedAt = Date.now();
|
|
938
931
|
completion.timer = window.setTimeout(() => {
|
|
939
932
|
completion.timer = null;
|
|
940
|
-
|
|
933
|
+
removeCard(completion.app);
|
|
941
934
|
}, completion.remaining);
|
|
942
935
|
}
|
|
943
936
|
|
|
944
937
|
function setCompletionPaused(completion, reason, paused) {
|
|
938
|
+
if (!completion || !completion.revealed) return;
|
|
945
939
|
if (paused) {
|
|
946
940
|
if (completion.paused.has(reason)) return;
|
|
947
941
|
if (!completion.paused.size && completion.timer) {
|
|
@@ -957,60 +951,6 @@
|
|
|
957
951
|
if (!completion.paused.size) scheduleCompletion(completion);
|
|
958
952
|
}
|
|
959
953
|
|
|
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
|
-
|
|
1014
954
|
function automaticSettingsKey(app) {
|
|
1015
955
|
return `pinokio:vault:auto-settings:${encodeURIComponent(app)}`;
|
|
1016
956
|
}
|
|
@@ -1024,24 +964,6 @@
|
|
|
1024
964
|
try {
|
|
1025
965
|
sessionStorage.setItem(storageKey, '1');
|
|
1026
966
|
} catch (_) {}
|
|
1027
|
-
let frameId = state.activeLeafId;
|
|
1028
|
-
if (!frameId || !leafElements.has(frameId)) {
|
|
1029
|
-
frameId = leafElements.keys().next().value || null;
|
|
1030
|
-
}
|
|
1031
|
-
const node = frameId ? nodeById.get(frameId) : null;
|
|
1032
|
-
const entry = frameId ? leafElements.get(frameId) : null;
|
|
1033
|
-
let currentPath = '';
|
|
1034
|
-
let targetPath = '';
|
|
1035
|
-
try {
|
|
1036
|
-
currentPath = new URL(node?.src || '', window.location.href).pathname;
|
|
1037
|
-
targetPath = new URL(href, window.location.href).pathname;
|
|
1038
|
-
} catch (_) {}
|
|
1039
|
-
if (entry && entry.vaultAutoSettingsReady &&
|
|
1040
|
-
currentPath && currentPath === targetPath) {
|
|
1041
|
-
entry.iframe?.contentWindow?.postMessage(
|
|
1042
|
-
{ e: 'vault-auto-settings' }, window.location.origin);
|
|
1043
|
-
return true;
|
|
1044
|
-
}
|
|
1045
967
|
const opened = navigateActiveLeaf(href);
|
|
1046
968
|
if (!opened) {
|
|
1047
969
|
try { sessionStorage.removeItem(storageKey); } catch (_) {}
|
|
@@ -1067,166 +989,290 @@
|
|
|
1067
989
|
return opened;
|
|
1068
990
|
}
|
|
1069
991
|
|
|
992
|
+
function createCard(app) {
|
|
993
|
+
const item = document.createElement('section');
|
|
994
|
+
item.className = 'vault-auto-scan-row';
|
|
995
|
+
|
|
996
|
+
const close = document.createElement('button');
|
|
997
|
+
close.type = 'button';
|
|
998
|
+
close.className = 'vault-auto-scan-close';
|
|
999
|
+
close.setAttribute('aria-label',
|
|
1000
|
+
`Dismiss Disk Saver notification for ${app}`);
|
|
1001
|
+
close.title = 'Dismiss';
|
|
1002
|
+
close.textContent = '×';
|
|
1003
|
+
|
|
1004
|
+
const header = document.createElement('header');
|
|
1005
|
+
header.className = 'vault-auto-scan-header';
|
|
1006
|
+
const product = document.createElement('span');
|
|
1007
|
+
product.className = 'vault-auto-scan-product';
|
|
1008
|
+
product.textContent = 'Disk Saver';
|
|
1009
|
+
const separator = document.createElement('span');
|
|
1010
|
+
separator.className = 'vault-auto-scan-separator';
|
|
1011
|
+
separator.setAttribute('aria-hidden', 'true');
|
|
1012
|
+
separator.textContent = '·';
|
|
1013
|
+
const appName = document.createElement('span');
|
|
1014
|
+
appName.className = 'vault-auto-scan-app';
|
|
1015
|
+
appName.textContent = app;
|
|
1016
|
+
appName.title = app;
|
|
1017
|
+
header.append(product, separator, appName);
|
|
1018
|
+
|
|
1019
|
+
const messages = document.createElement('div');
|
|
1020
|
+
messages.className = 'vault-auto-scan-messages';
|
|
1021
|
+
const controls = document.createElement('div');
|
|
1022
|
+
controls.className = 'vault-auto-scan-controls';
|
|
1023
|
+
item.append(close, header, messages, controls);
|
|
1024
|
+
|
|
1025
|
+
const card = {
|
|
1026
|
+
app,
|
|
1027
|
+
item,
|
|
1028
|
+
messages,
|
|
1029
|
+
controls,
|
|
1030
|
+
currentState: null,
|
|
1031
|
+
currentNoticeId: null,
|
|
1032
|
+
checkingShownAt: 0,
|
|
1033
|
+
dismissPending: false,
|
|
1034
|
+
pointerInside: false,
|
|
1035
|
+
focusInside: false,
|
|
1036
|
+
completion: null
|
|
1037
|
+
};
|
|
1038
|
+
close.addEventListener('click', async () => {
|
|
1039
|
+
if (card.completion) {
|
|
1040
|
+
removeCard(app);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
card.dismissPending = card.currentState === 'checking';
|
|
1044
|
+
close.disabled = true;
|
|
1045
|
+
try {
|
|
1046
|
+
const result = await requestAction(
|
|
1047
|
+
'automatic_dismiss', app, card.currentNoticeId);
|
|
1048
|
+
if (result.stale) {
|
|
1049
|
+
card.dismissPending = false;
|
|
1050
|
+
await loadState();
|
|
1051
|
+
if (close.isConnected) close.disabled = false;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
removeCard(app);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
console.warn('[Disk Saver] Automatic check dismissal failed', error);
|
|
1057
|
+
card.dismissPending = false;
|
|
1058
|
+
close.disabled = false;
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
item.addEventListener('mouseenter', () => {
|
|
1062
|
+
card.pointerInside = true;
|
|
1063
|
+
setCompletionPaused(card.completion, 'pointer', true);
|
|
1064
|
+
});
|
|
1065
|
+
item.addEventListener('mouseleave', () => {
|
|
1066
|
+
card.pointerInside = false;
|
|
1067
|
+
setCompletionPaused(card.completion, 'pointer', false);
|
|
1068
|
+
});
|
|
1069
|
+
item.addEventListener('focusin', () => {
|
|
1070
|
+
card.focusInside = true;
|
|
1071
|
+
setCompletionPaused(card.completion, 'focus', true);
|
|
1072
|
+
});
|
|
1073
|
+
item.addEventListener('focusout', (event) => {
|
|
1074
|
+
if (!item.contains(event.relatedTarget)) {
|
|
1075
|
+
card.focusInside = false;
|
|
1076
|
+
setCompletionPaused(card.completion, 'focus', false);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
cards.set(app, card);
|
|
1080
|
+
return card;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function messageIcon(state) {
|
|
1084
|
+
if (state === 'paused') return PAUSE_ICON;
|
|
1085
|
+
if (state === 'result') return DRIVE_ICON;
|
|
1086
|
+
if (state === 'complete') return COMPLETE_ICON;
|
|
1087
|
+
return '';
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function appendMessage(card, state) {
|
|
1091
|
+
const previous = card.messages.querySelector('[data-current="true"]');
|
|
1092
|
+
if (previous) {
|
|
1093
|
+
previous.dataset.current = 'false';
|
|
1094
|
+
previous.removeAttribute('aria-current');
|
|
1095
|
+
}
|
|
1096
|
+
const message = document.createElement('div');
|
|
1097
|
+
message.className = 'vault-auto-scan-message';
|
|
1098
|
+
message.dataset.state = state === 'checking-again' ? 'checking' : state;
|
|
1099
|
+
message.dataset.current = 'true';
|
|
1100
|
+
message.setAttribute('aria-current', 'true');
|
|
1101
|
+
const icon = document.createElement('span');
|
|
1102
|
+
icon.className = 'vault-auto-scan-icon';
|
|
1103
|
+
icon.innerHTML = messageIcon(state);
|
|
1104
|
+
const status = document.createElement('span');
|
|
1105
|
+
status.className = 'vault-auto-scan-status';
|
|
1106
|
+
status.textContent = statusText(state);
|
|
1107
|
+
message.append(icon, status);
|
|
1108
|
+
card.messages.appendChild(message);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function addSettingsButton(card) {
|
|
1112
|
+
const settings = document.createElement('button');
|
|
1113
|
+
settings.type = 'button';
|
|
1114
|
+
settings.className = 'vault-auto-scan-settings';
|
|
1115
|
+
settings.textContent = 'Automatic check settings';
|
|
1116
|
+
settings.addEventListener('click', async () => {
|
|
1117
|
+
settings.disabled = true;
|
|
1118
|
+
try {
|
|
1119
|
+
const result = await requestAction(
|
|
1120
|
+
'automatic_settings', card.app);
|
|
1121
|
+
if (!result.href || !openAutomaticSettings(card.app, result.href)) {
|
|
1122
|
+
throw new Error('The app page is unavailable.');
|
|
1123
|
+
}
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
console.warn('[Disk Saver] Automatic check settings failed', error);
|
|
1126
|
+
} finally {
|
|
1127
|
+
if (settings.isConnected) settings.disabled = false;
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
card.controls.appendChild(settings);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function addStateAction(card, row) {
|
|
1134
|
+
const action = actionFor(row.state);
|
|
1135
|
+
const button = document.createElement('button');
|
|
1136
|
+
button.type = 'button';
|
|
1137
|
+
button.className = 'vault-auto-scan-action';
|
|
1138
|
+
button.textContent = action.label;
|
|
1139
|
+
button.addEventListener('click', async () => {
|
|
1140
|
+
button.disabled = true;
|
|
1141
|
+
try {
|
|
1142
|
+
const result = await requestAction(
|
|
1143
|
+
action.action, card.app, row.notice_id);
|
|
1144
|
+
if (result.stale) {
|
|
1145
|
+
await loadState();
|
|
1146
|
+
if (button.isConnected) button.disabled = false;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (action.action === 'automatic_review' && result.href) {
|
|
1150
|
+
if (!openAutomaticReview(card.app, result.href)) {
|
|
1151
|
+
throw new Error('The app page could not be opened.');
|
|
1152
|
+
}
|
|
1153
|
+
removeCard(card.app);
|
|
1154
|
+
} else {
|
|
1155
|
+
await loadState();
|
|
1156
|
+
}
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.warn('[Disk Saver] Automatic check action failed', error);
|
|
1159
|
+
if (button.isConnected) button.disabled = false;
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
card.controls.appendChild(button);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function renderControls(card, row) {
|
|
1166
|
+
card.controls.replaceChildren();
|
|
1167
|
+
if (row.state === 'complete') return;
|
|
1168
|
+
if (row.state !== 'result') addSettingsButton(card);
|
|
1169
|
+
addStateAction(card, row);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function updateCard(row) {
|
|
1173
|
+
let card = cards.get(row.app);
|
|
1174
|
+
if (!card) card = createCard(row.app);
|
|
1175
|
+
const unchanged = card.currentState === row.state &&
|
|
1176
|
+
card.currentNoticeId === (row.notice_id || null);
|
|
1177
|
+
if (unchanged) return card;
|
|
1178
|
+
stopCompletion(card);
|
|
1179
|
+
const displayState = row.state === 'checking' &&
|
|
1180
|
+
card.currentState === 'paused' ? 'checking-again' : row.state;
|
|
1181
|
+
appendMessage(card, displayState);
|
|
1182
|
+
card.currentState = row.state;
|
|
1183
|
+
card.currentNoticeId = row.notice_id || null;
|
|
1184
|
+
card.checkingShownAt = row.state === 'checking' ? Date.now() : 0;
|
|
1185
|
+
card.dismissPending = false;
|
|
1186
|
+
card.item.dataset.state = row.state;
|
|
1187
|
+
renderControls(card, row);
|
|
1188
|
+
return card;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function revealCompletion(completion) {
|
|
1192
|
+
const card = cards.get(completion.app);
|
|
1193
|
+
if (!card || card.completion !== completion ||
|
|
1194
|
+
card.currentState !== 'checking' || card.dismissPending) return;
|
|
1195
|
+
completion.timer = null;
|
|
1196
|
+
completion.revealed = true;
|
|
1197
|
+
appendMessage(card, 'complete');
|
|
1198
|
+
card.currentState = 'complete';
|
|
1199
|
+
card.currentNoticeId = null;
|
|
1200
|
+
card.item.dataset.state = 'complete';
|
|
1201
|
+
renderControls(card, { state: 'complete' });
|
|
1202
|
+
if (card.pointerInside) completion.paused.add('pointer');
|
|
1203
|
+
if (card.focusInside) completion.paused.add('focus');
|
|
1204
|
+
scheduleCompletion(completion);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function startCompletion(app) {
|
|
1208
|
+
const card = cards.get(app);
|
|
1209
|
+
if (!card || card.currentState !== 'checking' ||
|
|
1210
|
+
card.dismissPending || card.completion) return false;
|
|
1211
|
+
const completion = {
|
|
1212
|
+
app,
|
|
1213
|
+
revealed: false,
|
|
1214
|
+
timer: null,
|
|
1215
|
+
remaining: COMPLETION_VISIBLE_MS,
|
|
1216
|
+
startedAt: 0,
|
|
1217
|
+
paused: new Set()
|
|
1218
|
+
};
|
|
1219
|
+
card.completion = completion;
|
|
1220
|
+
const elapsed = Math.max(0, Date.now() - card.checkingShownAt);
|
|
1221
|
+
const delay = Math.max(0, MIN_CHECKING_VISIBLE_MS - elapsed);
|
|
1222
|
+
if (delay) {
|
|
1223
|
+
completion.timer = window.setTimeout(
|
|
1224
|
+
() => revealCompletion(completion), delay);
|
|
1225
|
+
} else {
|
|
1226
|
+
revealCompletion(completion);
|
|
1227
|
+
}
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1070
1231
|
function render(snapshot, options = {}) {
|
|
1071
1232
|
if (!snapshot || snapshot.global_scan_ready !== true) {
|
|
1072
|
-
|
|
1073
|
-
visibleCheckingApps = new Set();
|
|
1074
|
-
tray.replaceChildren();
|
|
1075
|
-
tray.hidden = true;
|
|
1233
|
+
resetCards();
|
|
1076
1234
|
return;
|
|
1077
1235
|
}
|
|
1078
|
-
const rows =
|
|
1236
|
+
const rows = Array.isArray(snapshot.rows)
|
|
1079
1237
|
? snapshot.rows
|
|
1080
1238
|
: [];
|
|
1081
1239
|
const validRows = rows.filter((row) =>
|
|
1082
1240
|
row && typeof row.app === 'string' && row.app);
|
|
1083
1241
|
const liveApps = new Set(validRows.map((row) => row.app));
|
|
1084
|
-
|
|
1085
|
-
if (liveApps.has(app)) removeCompletion(app);
|
|
1086
|
-
});
|
|
1087
|
-
const manualApps = new Set(snapshot && Array.isArray(snapshot.settings)
|
|
1242
|
+
const manualApps = new Set(Array.isArray(snapshot.settings)
|
|
1088
1243
|
? snapshot.settings.filter((setting) =>
|
|
1089
1244
|
setting && setting.mode === 'manual').map((setting) => setting.app)
|
|
1090
1245
|
: []);
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1246
|
+
const retained = new Set();
|
|
1247
|
+
validRows.forEach((row) => {
|
|
1248
|
+
updateCard(row);
|
|
1249
|
+
retained.add(row.app);
|
|
1250
|
+
});
|
|
1251
|
+
const completion = snapshot.completion;
|
|
1094
1252
|
if (options.acceptCompletion && completion &&
|
|
1095
1253
|
completion.outcome === 'no_possible_duplicates' &&
|
|
1096
1254
|
typeof completion.app === 'string' && completion.app &&
|
|
1097
|
-
visibleCheckingApps.has(completion.app) &&
|
|
1098
1255
|
!liveApps.has(completion.app) &&
|
|
1099
|
-
!manualApps.has(completion.app)
|
|
1100
|
-
|
|
1256
|
+
!manualApps.has(completion.app) &&
|
|
1257
|
+
startCompletion(completion.app)) {
|
|
1258
|
+
retained.add(completion.app);
|
|
1101
1259
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1260
|
+
cards.forEach((card, app) => {
|
|
1261
|
+
if (card.completion && !manualApps.has(app)) retained.add(app);
|
|
1262
|
+
});
|
|
1263
|
+
[...cards.keys()].forEach((app) => {
|
|
1264
|
+
if (!retained.has(app)) removeCard(app);
|
|
1265
|
+
});
|
|
1104
1266
|
validRows.forEach((row) => {
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
item.dataset.state = row.state || 'checking';
|
|
1108
|
-
|
|
1109
|
-
const close = document.createElement('button');
|
|
1110
|
-
close.type = 'button';
|
|
1111
|
-
close.className = 'vault-auto-scan-close';
|
|
1112
|
-
close.setAttribute('aria-label', `Dismiss Disk Saver notification for ${row.app}`);
|
|
1113
|
-
close.title = 'Dismiss';
|
|
1114
|
-
close.textContent = '×';
|
|
1115
|
-
close.addEventListener('click', async () => {
|
|
1116
|
-
if (row.state === 'checking') visibleCheckingApps.delete(row.app);
|
|
1117
|
-
close.disabled = true;
|
|
1118
|
-
try {
|
|
1119
|
-
const result = await requestAction(
|
|
1120
|
-
'automatic_dismiss', row.app, row.notice_id);
|
|
1121
|
-
if (result.stale) {
|
|
1122
|
-
await loadState();
|
|
1123
|
-
if (close.isConnected) close.disabled = false;
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
removeRow(item);
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
console.warn('[Disk Saver] Automatic check dismissal failed', error);
|
|
1129
|
-
if (row.state === 'checking' && item.isConnected) {
|
|
1130
|
-
visibleCheckingApps.add(row.app);
|
|
1131
|
-
}
|
|
1132
|
-
close.disabled = false;
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
const icon = document.createElement('span');
|
|
1137
|
-
icon.className = 'vault-auto-scan-icon';
|
|
1138
|
-
if (row.state === 'paused') {
|
|
1139
|
-
icon.innerHTML = PAUSE_ICON;
|
|
1140
|
-
} else if (row.state === 'result') {
|
|
1141
|
-
icon.innerHTML = DRIVE_ICON;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const copy = document.createElement('span');
|
|
1145
|
-
copy.className = 'vault-auto-scan-copy';
|
|
1146
|
-
const appName = document.createElement('span');
|
|
1147
|
-
appName.className = 'vault-auto-scan-app';
|
|
1148
|
-
appName.textContent = row.app;
|
|
1149
|
-
appName.title = row.app;
|
|
1150
|
-
const status = document.createElement('span');
|
|
1151
|
-
status.className = 'vault-auto-scan-status';
|
|
1152
|
-
if (row.state === 'result') {
|
|
1153
|
-
const detail = document.createElement('span');
|
|
1154
|
-
detail.className = 'vault-auto-scan-detail';
|
|
1155
|
-
detail.textContent = 'may have duplicate files';
|
|
1156
|
-
status.append(detail);
|
|
1157
|
-
} else {
|
|
1158
|
-
status.textContent = statusText(row);
|
|
1159
|
-
}
|
|
1160
|
-
copy.append(appName, status);
|
|
1161
|
-
|
|
1162
|
-
const controls = document.createElement('span');
|
|
1163
|
-
controls.className = 'vault-auto-scan-controls';
|
|
1164
|
-
if (row.state !== 'result') {
|
|
1165
|
-
const settings = document.createElement('button');
|
|
1166
|
-
settings.type = 'button';
|
|
1167
|
-
settings.className = 'vault-auto-scan-settings';
|
|
1168
|
-
settings.textContent = 'Automatic check settings';
|
|
1169
|
-
settings.addEventListener('click', async () => {
|
|
1170
|
-
settings.disabled = true;
|
|
1171
|
-
try {
|
|
1172
|
-
const result = await requestAction(
|
|
1173
|
-
'automatic_settings', row.app);
|
|
1174
|
-
if (!result.href || !openAutomaticSettings(row.app, result.href)) {
|
|
1175
|
-
throw new Error('The app page is unavailable.');
|
|
1176
|
-
}
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
console.warn('[Disk Saver] Automatic check settings failed', error);
|
|
1179
|
-
} finally {
|
|
1180
|
-
if (settings.isConnected) settings.disabled = false;
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
copy.appendChild(settings);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
const action = actionFor(row);
|
|
1187
|
-
const button = document.createElement('button');
|
|
1188
|
-
button.type = 'button';
|
|
1189
|
-
button.className = 'vault-auto-scan-action';
|
|
1190
|
-
button.textContent = action.label;
|
|
1191
|
-
button.addEventListener('click', async () => {
|
|
1192
|
-
button.disabled = true;
|
|
1193
|
-
try {
|
|
1194
|
-
const result = await requestAction(
|
|
1195
|
-
action.action, row.app, row.notice_id);
|
|
1196
|
-
if (result.stale) {
|
|
1197
|
-
await loadState();
|
|
1198
|
-
if (button.isConnected) button.disabled = false;
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
if (action.action === 'automatic_review' && result.href) {
|
|
1202
|
-
if (!openAutomaticReview(row.app, result.href)) {
|
|
1203
|
-
throw new Error('The app page could not be opened.');
|
|
1204
|
-
}
|
|
1205
|
-
removeRow(item);
|
|
1206
|
-
} else {
|
|
1207
|
-
await loadState();
|
|
1208
|
-
}
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
console.warn('[Disk Saver] Automatic check action failed', error);
|
|
1211
|
-
button.disabled = false;
|
|
1212
|
-
}
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
controls.appendChild(button);
|
|
1216
|
-
|
|
1217
|
-
item.append(close, icon, copy, controls);
|
|
1218
|
-
fragment.appendChild(item);
|
|
1267
|
+
const card = cards.get(row.app);
|
|
1268
|
+
if (card) tray.appendChild(card.item);
|
|
1219
1269
|
});
|
|
1220
|
-
|
|
1221
|
-
if (!liveApps.has(
|
|
1222
|
-
|
|
1270
|
+
cards.forEach((card, app) => {
|
|
1271
|
+
if (card.completion && !liveApps.has(app)) {
|
|
1272
|
+
tray.appendChild(card.item);
|
|
1223
1273
|
}
|
|
1224
1274
|
});
|
|
1225
|
-
tray.
|
|
1226
|
-
tray.hidden = tray.childElementCount === 0;
|
|
1227
|
-
visibleCheckingApps = new Set(validRows
|
|
1228
|
-
.filter((row) => row.state === 'checking')
|
|
1229
|
-
.map((row) => row.app));
|
|
1275
|
+
tray.hidden = cards.size === 0;
|
|
1230
1276
|
}
|
|
1231
1277
|
|
|
1232
1278
|
async function loadState() {
|
|
@@ -1262,7 +1308,7 @@
|
|
|
1262
1308
|
}
|
|
1263
1309
|
};
|
|
1264
1310
|
eventSource.onerror = () => {
|
|
1265
|
-
|
|
1311
|
+
resetCards();
|
|
1266
1312
|
loadState();
|
|
1267
1313
|
};
|
|
1268
1314
|
}
|
package/server/public/vault.js
CHANGED
|
@@ -582,14 +582,6 @@ const connectAutomaticMode = () => {
|
|
|
582
582
|
} catch (error) {}
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
|
-
const requestAutomaticModeMenu = () => {
|
|
586
|
-
if (!IS_APP_MODE) return
|
|
587
|
-
if (automaticSettingsKey) {
|
|
588
|
-
try { sessionStorage.removeItem(automaticSettingsKey) } catch (error) {}
|
|
589
|
-
}
|
|
590
|
-
state.automaticModeMenuRequested = true
|
|
591
|
-
if (state.data) renderOverview()
|
|
592
|
-
}
|
|
593
585
|
const sourceById = (id) => (state.data.sources || []).find((source) => source.id === id)
|
|
594
586
|
const sourceChildren = (id) => (state.data.sources || []).filter((source) => source.parent_id === id)
|
|
595
587
|
const sourceIsWithinScope = (sourceId) => {
|
|
@@ -4186,12 +4178,6 @@ if (IS_APP_MODE) {
|
|
|
4186
4178
|
const trigger = menu.querySelector("summary")
|
|
4187
4179
|
if (trigger) trigger.focus()
|
|
4188
4180
|
}, true)
|
|
4189
|
-
window.addEventListener("message", (event) => {
|
|
4190
|
-
if (event.source !== window.parent ||
|
|
4191
|
-
event.origin !== window.location.origin ||
|
|
4192
|
-
!event.data || event.data.e !== "vault-auto-settings") return
|
|
4193
|
-
requestAutomaticModeMenu()
|
|
4194
|
-
})
|
|
4195
4181
|
loadAutomaticMode().finally(connectAutomaticMode)
|
|
4196
4182
|
window.addEventListener("beforeunload", () => {
|
|
4197
4183
|
if (automaticModeEventSource) automaticModeEventSource.close()
|
package/server/views/app.ejs
CHANGED
|
@@ -12889,28 +12889,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12889
12889
|
|
|
12890
12890
|
|
|
12891
12891
|
});
|
|
12892
|
-
const openVaultAutomaticSettings = () => {
|
|
12893
|
-
const frame = document.querySelector("iframe[name='app-vault']")
|
|
12894
|
-
if (!frame || !frame.contentWindow) return false
|
|
12895
|
-
frame.contentWindow.postMessage(
|
|
12896
|
-
{ e: "vault-auto-settings" }, window.location.origin)
|
|
12897
|
-
return true
|
|
12898
|
-
}
|
|
12899
|
-
window.addEventListener("message", async (event) => {
|
|
12900
|
-
if (event.source !== window.parent ||
|
|
12901
|
-
event.origin !== window.location.origin ||
|
|
12902
|
-
!event.data || event.data.e !== "vault-auto-settings") return
|
|
12903
|
-
const diskSaverTab = document.querySelector("#save-space-tab")
|
|
12904
|
-
if (!diskSaverTab) return
|
|
12905
|
-
if (diskSaverTab.classList.contains("selected") &&
|
|
12906
|
-
openVaultAutomaticSettings()) return
|
|
12907
|
-
await renderSelection({ target: diskSaverTab, force: true })
|
|
12908
|
-
openVaultAutomaticSettings()
|
|
12909
|
-
})
|
|
12910
|
-
if (window.parent !== window) {
|
|
12911
|
-
window.parent.postMessage(
|
|
12912
|
-
{ e: "vault-auto-settings-ready" }, window.location.origin)
|
|
12913
|
-
}
|
|
12914
12892
|
renderSelection({ force: true })
|
|
12915
12893
|
<% if (type === "browse" || type === "files") { %>
|
|
12916
12894
|
const repoStatusCache = new Map()
|
package/server/views/layout.ejs
CHANGED
|
@@ -161,15 +161,12 @@
|
|
|
161
161
|
|
|
162
162
|
.vault-auto-scan-row {
|
|
163
163
|
position: relative;
|
|
164
|
-
display:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
align-items: center;
|
|
168
|
-
column-gap: 10px;
|
|
169
|
-
min-height: 86px;
|
|
170
|
-
padding: 9px 34px 9px 12px;
|
|
164
|
+
display: flex;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
padding: 10px 12px;
|
|
171
167
|
box-sizing: border-box;
|
|
172
|
-
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: 8px;
|
|
173
170
|
color: var(--vault-notice-text);
|
|
174
171
|
background: var(--vault-notice-surface);
|
|
175
172
|
border: 1px solid var(--vault-notice-border);
|
|
@@ -179,10 +176,61 @@
|
|
|
179
176
|
animation: vault-notice-enter 180ms cubic-bezier(0.25, 1, 0.5, 1);
|
|
180
177
|
}
|
|
181
178
|
|
|
179
|
+
.vault-auto-scan-header {
|
|
180
|
+
display: flex;
|
|
181
|
+
min-width: 0;
|
|
182
|
+
min-height: 18px;
|
|
183
|
+
padding-right: 24px;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 6px;
|
|
186
|
+
font: 500 11px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.vault-auto-scan-product {
|
|
190
|
+
color: var(--vault-notice-text);
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.vault-auto-scan-separator {
|
|
196
|
+
color: var(--vault-notice-muted);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.vault-auto-scan-app {
|
|
200
|
+
min-width: 0;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
color: var(--vault-notice-muted);
|
|
203
|
+
text-overflow: ellipsis;
|
|
204
|
+
white-space: nowrap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.vault-auto-scan-messages {
|
|
208
|
+
display: grid;
|
|
209
|
+
min-width: 0;
|
|
210
|
+
gap: 4px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.vault-auto-scan-message {
|
|
214
|
+
display: grid;
|
|
215
|
+
min-width: 0;
|
|
216
|
+
min-height: 22px;
|
|
217
|
+
grid-template-columns: 20px minmax(0, 1fr);
|
|
218
|
+
align-items: center;
|
|
219
|
+
column-gap: 8px;
|
|
220
|
+
transition: opacity 120ms cubic-bezier(0.25, 1, 0.5, 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.vault-auto-scan-message[data-current="false"] {
|
|
224
|
+
opacity: 0.58;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.vault-auto-scan-message[data-current="true"] {
|
|
228
|
+
animation: vault-notice-message-enter 160ms
|
|
229
|
+
cubic-bezier(0.25, 1, 0.5, 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
182
232
|
.vault-auto-scan-icon {
|
|
183
233
|
display: grid;
|
|
184
|
-
grid-column: 1;
|
|
185
|
-
grid-row: 1 / 4;
|
|
186
234
|
place-items: center;
|
|
187
235
|
width: 20px;
|
|
188
236
|
height: 20px;
|
|
@@ -199,57 +247,43 @@
|
|
|
199
247
|
stroke-width: 1.8;
|
|
200
248
|
}
|
|
201
249
|
|
|
202
|
-
.vault-auto-scan-
|
|
250
|
+
.vault-auto-scan-message[data-state="checking"] .vault-auto-scan-icon {
|
|
203
251
|
border: 1.5px solid var(--vault-notice-border);
|
|
204
|
-
border-top-color: var(--vault-notice-text);
|
|
205
252
|
border-radius: 50%;
|
|
206
253
|
box-sizing: border-box;
|
|
207
|
-
animation: vault-notice-spin 900ms linear infinite;
|
|
208
254
|
}
|
|
209
255
|
|
|
210
|
-
.vault-auto-scan-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
grid-template-rows: 16px 34px 18px;
|
|
215
|
-
align-items: center;
|
|
216
|
-
min-width: 0;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.vault-auto-scan-app {
|
|
220
|
-
overflow: hidden;
|
|
221
|
-
color: var(--vault-notice-muted);
|
|
222
|
-
font-size: 11px;
|
|
223
|
-
font-weight: 500;
|
|
224
|
-
line-height: 16px;
|
|
225
|
-
text-overflow: ellipsis;
|
|
226
|
-
white-space: nowrap;
|
|
256
|
+
.vault-auto-scan-message[data-current="true"][data-state="checking"]
|
|
257
|
+
.vault-auto-scan-icon {
|
|
258
|
+
border-top-color: var(--vault-notice-text);
|
|
259
|
+
animation: vault-notice-spin 900ms linear infinite;
|
|
227
260
|
}
|
|
228
261
|
|
|
229
262
|
.vault-auto-scan-status {
|
|
230
|
-
display: flex;
|
|
231
263
|
min-width: 0;
|
|
232
|
-
align-items: baseline;
|
|
233
264
|
overflow: hidden;
|
|
234
265
|
font-size: 13px;
|
|
235
266
|
font-weight: 600;
|
|
236
|
-
line-height:
|
|
267
|
+
line-height: 20px;
|
|
237
268
|
text-overflow: ellipsis;
|
|
238
269
|
white-space: nowrap;
|
|
239
270
|
}
|
|
240
271
|
|
|
241
|
-
.vault-auto-scan-
|
|
242
|
-
|
|
243
|
-
color: var(--vault-notice-muted);
|
|
244
|
-
font-weight: 400;
|
|
245
|
-
text-overflow: ellipsis;
|
|
272
|
+
.vault-auto-scan-message[data-current="false"] .vault-auto-scan-status {
|
|
273
|
+
font-weight: 500;
|
|
246
274
|
}
|
|
247
275
|
|
|
248
276
|
.vault-auto-scan-controls {
|
|
249
|
-
display:
|
|
250
|
-
|
|
251
|
-
|
|
277
|
+
display: flex;
|
|
278
|
+
min-height: 32px;
|
|
279
|
+
padding-left: 28px;
|
|
252
280
|
align-items: center;
|
|
281
|
+
justify-content: flex-end;
|
|
282
|
+
gap: 8px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.vault-auto-scan-controls:empty {
|
|
286
|
+
display: none;
|
|
253
287
|
}
|
|
254
288
|
|
|
255
289
|
.vault-auto-scan-settings,
|
|
@@ -261,7 +295,7 @@
|
|
|
261
295
|
}
|
|
262
296
|
|
|
263
297
|
.vault-auto-scan-settings {
|
|
264
|
-
|
|
298
|
+
margin-right: auto;
|
|
265
299
|
min-height: 18px;
|
|
266
300
|
padding: 0;
|
|
267
301
|
font: 500 10.5px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
@@ -293,8 +327,7 @@
|
|
|
293
327
|
}
|
|
294
328
|
|
|
295
329
|
.vault-auto-scan-action {
|
|
296
|
-
|
|
297
|
-
width: 76px;
|
|
330
|
+
min-width: 76px;
|
|
298
331
|
min-height: 32px;
|
|
299
332
|
padding: 6px 10px;
|
|
300
333
|
color: var(--vault-notice-text);
|
|
@@ -333,32 +366,7 @@
|
|
|
333
366
|
opacity: 0.55;
|
|
334
367
|
}
|
|
335
368
|
|
|
336
|
-
.vault-auto-scan-
|
|
337
|
-
.vault-auto-scan-row[data-state="complete"] {
|
|
338
|
-
grid-template-rows: 16px 30px;
|
|
339
|
-
min-height: 66px;
|
|
340
|
-
padding-top: 10px;
|
|
341
|
-
padding-bottom: 10px;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-icon,
|
|
345
|
-
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
|
|
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 {
|
|
349
|
-
grid-row: 1 / 3;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
|
|
353
|
-
.vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
|
|
354
|
-
grid-template-rows: 16px 30px;
|
|
355
|
-
}
|
|
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 {
|
|
369
|
+
.vault-auto-scan-message[data-state="complete"] .vault-auto-scan-icon {
|
|
362
370
|
color: var(--vault-notice-accent);
|
|
363
371
|
}
|
|
364
372
|
|
|
@@ -371,25 +379,28 @@
|
|
|
371
379
|
to { transform: rotate(360deg); }
|
|
372
380
|
}
|
|
373
381
|
|
|
382
|
+
@keyframes vault-notice-message-enter {
|
|
383
|
+
from { opacity: 0; transform: translateY(2px); }
|
|
384
|
+
to { opacity: 1; transform: translateY(0); }
|
|
385
|
+
}
|
|
386
|
+
|
|
374
387
|
@media (pointer: coarse) {
|
|
375
|
-
.vault-auto-scan-
|
|
376
|
-
.vault-auto-scan-copy {
|
|
377
|
-
grid-template-rows: 16px 44px 28px;
|
|
378
|
-
}
|
|
379
|
-
.vault-auto-scan-row {
|
|
380
|
-
min-height: 106px;
|
|
381
|
-
}
|
|
388
|
+
.vault-auto-scan-controls { min-height: 44px; }
|
|
382
389
|
.vault-auto-scan-action {
|
|
383
390
|
min-height: 44px;
|
|
384
391
|
}
|
|
385
|
-
.vault-auto-scan-settings { min-height:
|
|
392
|
+
.vault-auto-scan-settings { min-height: 44px; }
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
@media (prefers-reduced-motion: reduce) {
|
|
389
396
|
.vault-auto-scan-row {
|
|
390
397
|
animation: none;
|
|
391
398
|
}
|
|
392
|
-
.vault-auto-scan-
|
|
399
|
+
.vault-auto-scan-message[data-current="true"] {
|
|
400
|
+
animation: none;
|
|
401
|
+
}
|
|
402
|
+
.vault-auto-scan-message[data-current="true"][data-state="checking"]
|
|
403
|
+
.vault-auto-scan-icon {
|
|
393
404
|
animation-duration: 1800ms;
|
|
394
405
|
}
|
|
395
406
|
}
|
|
@@ -101,10 +101,15 @@ test("the shared layout renders and reviews automatic possible-match notices", a
|
|
|
101
101
|
|
|
102
102
|
const tray = dom.window.document.getElementById("vault-auto-scan-tray")
|
|
103
103
|
assert.equal(tray.hidden, false)
|
|
104
|
+
assert.equal(tray.querySelector(
|
|
105
|
+
".vault-auto-scan-product").textContent, "Disk Saver")
|
|
104
106
|
assert.equal(tray.querySelector(
|
|
105
107
|
".vault-auto-scan-app").textContent, "ComfyUI")
|
|
106
108
|
assert.equal(tray.querySelector(
|
|
107
|
-
".vault-auto-scan-
|
|
109
|
+
".vault-auto-scan-status").textContent,
|
|
110
|
+
"Possible duplicate files found")
|
|
111
|
+
assert.equal(tray.querySelectorAll(
|
|
112
|
+
".vault-auto-scan-message").length, 1)
|
|
108
113
|
assert.equal(tray.querySelector(".vault-auto-scan-value"), null)
|
|
109
114
|
assert.equal(tray.querySelector(
|
|
110
115
|
".vault-auto-scan-action").textContent, "Review")
|
|
@@ -131,9 +136,119 @@ test("the shared layout renders and reviews automatic possible-match notices", a
|
|
|
131
136
|
assert.equal(requests.some((request) =>
|
|
132
137
|
request && request.url === "/info/vault/automatic-scans"), false)
|
|
133
138
|
assert.match(script, /window\.location\.assign\(/)
|
|
134
|
-
assert.ok(script.indexOf("if (!openAutomaticReview(
|
|
135
|
-
script.indexOf("
|
|
136
|
-
"if (!openAutomaticReview(
|
|
139
|
+
assert.ok(script.indexOf("if (!openAutomaticReview(card.app, result.href))") <
|
|
140
|
+
script.indexOf("removeCard(card.app);", script.indexOf(
|
|
141
|
+
"if (!openAutomaticReview(card.app, result.href))")))
|
|
142
|
+
|
|
143
|
+
dom.window.close()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test("automatic check states append within one card", async () => {
|
|
147
|
+
const template = await fs.promises.readFile(
|
|
148
|
+
path.join(root, "server", "views", "layout.ejs"), "utf8")
|
|
149
|
+
const script = await fs.promises.readFile(
|
|
150
|
+
path.join(root, "server", "public", "layout.js"), "utf8")
|
|
151
|
+
const html = ejs.render(template, {
|
|
152
|
+
theme: "light",
|
|
153
|
+
agent: "web",
|
|
154
|
+
initialPath: "/home",
|
|
155
|
+
defaultPath: "/home",
|
|
156
|
+
sessionId: null,
|
|
157
|
+
vaultEnabled: true
|
|
158
|
+
})
|
|
159
|
+
const dom = new JSDOM(html, {
|
|
160
|
+
runScripts: "outside-only",
|
|
161
|
+
pretendToBeVisual: true,
|
|
162
|
+
url: "http://localhost/"
|
|
163
|
+
})
|
|
164
|
+
const eventSources = []
|
|
165
|
+
dom.window.EventSource = class EventSource {
|
|
166
|
+
constructor(url) {
|
|
167
|
+
this.url = url
|
|
168
|
+
eventSources.push(this)
|
|
169
|
+
}
|
|
170
|
+
close() {}
|
|
171
|
+
}
|
|
172
|
+
dom.window.fetch = async (url) => {
|
|
173
|
+
throw new Error(`Unexpected request: ${url}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
dom.window.eval(script)
|
|
177
|
+
await waitFor(() => eventSources.length === 1)
|
|
178
|
+
const send = (rows, settings = []) => eventSources[0].onmessage({
|
|
179
|
+
data: JSON.stringify({
|
|
180
|
+
enabled: true,
|
|
181
|
+
global_scan_ready: true,
|
|
182
|
+
rows,
|
|
183
|
+
settings
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
const row = (state, noticeId) => ({
|
|
187
|
+
app: "ComfyUI",
|
|
188
|
+
state,
|
|
189
|
+
notice_id: noticeId
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
send([row("checking", "checking:1:")])
|
|
193
|
+
const tray = dom.window.document.getElementById("vault-auto-scan-tray")
|
|
194
|
+
const card = tray.querySelector(".vault-auto-scan-row")
|
|
195
|
+
assert.ok(card)
|
|
196
|
+
|
|
197
|
+
send([row("paused", "paused:2:")], [
|
|
198
|
+
{ app: "ComfyUI", mode: "manual" }
|
|
199
|
+
])
|
|
200
|
+
assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
|
|
201
|
+
assert.deepEqual([...card.querySelectorAll(".vault-auto-scan-status")]
|
|
202
|
+
.map((status) => status.textContent), [
|
|
203
|
+
"Checking for possible duplicate files...",
|
|
204
|
+
"Automatic checks are paused"
|
|
205
|
+
])
|
|
206
|
+
assert.equal(card.querySelectorAll(
|
|
207
|
+
'.vault-auto-scan-message[data-current="true"]').length, 1)
|
|
208
|
+
assert.equal(card.querySelector(
|
|
209
|
+
'.vault-auto-scan-message[data-current="false"] button'), null)
|
|
210
|
+
assert.equal(card.querySelector(
|
|
211
|
+
".vault-auto-scan-action").textContent, "Resume")
|
|
212
|
+
|
|
213
|
+
send([row("checking", "checking:3:")], [
|
|
214
|
+
{ app: "ComfyUI", mode: "automatic" }
|
|
215
|
+
])
|
|
216
|
+
assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
|
|
217
|
+
assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 3)
|
|
218
|
+
assert.equal(card.querySelector(
|
|
219
|
+
'.vault-auto-scan-message[data-current="true"] .vault-auto-scan-status')
|
|
220
|
+
.textContent, "Checking again for possible duplicate files...")
|
|
221
|
+
assert.equal(card.querySelector(
|
|
222
|
+
".vault-auto-scan-action").textContent, "Pause")
|
|
223
|
+
|
|
224
|
+
send([row("result", "result:4:result-a")], [
|
|
225
|
+
{ app: "ComfyUI", mode: "automatic" }
|
|
226
|
+
])
|
|
227
|
+
assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
|
|
228
|
+
assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 4)
|
|
229
|
+
assert.equal(card.querySelector(
|
|
230
|
+
'.vault-auto-scan-message[data-current="true"] .vault-auto-scan-status')
|
|
231
|
+
.textContent, "Possible duplicate files found")
|
|
232
|
+
assert.equal(card.querySelector(".vault-auto-scan-settings"), null)
|
|
233
|
+
assert.equal(card.querySelector(
|
|
234
|
+
".vault-auto-scan-action").textContent, "Review")
|
|
235
|
+
|
|
236
|
+
send([row("result", "result:4:result-a")], [
|
|
237
|
+
{ app: "ComfyUI", mode: "automatic" }
|
|
238
|
+
])
|
|
239
|
+
assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 4,
|
|
240
|
+
"repeated snapshots do not duplicate the current message")
|
|
241
|
+
|
|
242
|
+
send([
|
|
243
|
+
row("result", "result:4:result-a"),
|
|
244
|
+
{ app: "OtherApp", state: "checking", notice_id: "checking:5:" }
|
|
245
|
+
], [
|
|
246
|
+
{ app: "ComfyUI", mode: "automatic" },
|
|
247
|
+
{ app: "OtherApp", mode: "automatic" }
|
|
248
|
+
])
|
|
249
|
+
assert.equal(tray.querySelectorAll(".vault-auto-scan-row").length, 2)
|
|
250
|
+
assert.deepEqual([...tray.querySelectorAll(".vault-auto-scan-app")]
|
|
251
|
+
.map((app) => app.textContent), ["ComfyUI", "OtherApp"])
|
|
137
252
|
|
|
138
253
|
dom.window.close()
|
|
139
254
|
})
|
|
@@ -205,7 +320,7 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
|
|
|
205
320
|
".vault-auto-scan-app").textContent, "ComfyUI")
|
|
206
321
|
assert.equal(tray.querySelector(
|
|
207
322
|
".vault-auto-scan-status").textContent,
|
|
208
|
-
"Checking for possible duplicate files
|
|
323
|
+
"Checking for possible duplicate files...")
|
|
209
324
|
assert.equal(tray.querySelector(
|
|
210
325
|
".vault-auto-scan-settings").textContent,
|
|
211
326
|
"Automatic check settings")
|
|
@@ -223,19 +338,9 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
|
|
|
223
338
|
assert.equal(tray.querySelector(
|
|
224
339
|
".vault-auto-scan-settings").disabled, false)
|
|
225
340
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
dom.window.dispatchEvent(new dom.window.MessageEvent("message", {
|
|
231
|
-
data: { e: "vault-auto-settings-ready" },
|
|
232
|
-
origin: dom.window.location.origin,
|
|
233
|
-
source: iframe.contentWindow
|
|
234
|
-
}))
|
|
235
|
-
tray.querySelector(".vault-auto-scan-settings").click()
|
|
236
|
-
await waitFor(() => relayed.length === 1)
|
|
237
|
-
assert.equal(relayed[0].message.e, "vault-auto-settings")
|
|
238
|
-
assert.equal(relayed[0].targetOrigin, dom.window.location.origin)
|
|
341
|
+
assert.doesNotMatch(script, /vaultAutoSettingsReady|vault-auto-settings-ready/)
|
|
342
|
+
assert.doesNotMatch(script,
|
|
343
|
+
/postMessage\(\s*\{ e: ['"]vault-auto-settings['"]/)
|
|
239
344
|
|
|
240
345
|
tray.querySelector(".vault-auto-scan-close").click()
|
|
241
346
|
await waitFor(() => tray.hidden)
|
|
@@ -265,10 +370,21 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
265
370
|
})
|
|
266
371
|
const eventSources = []
|
|
267
372
|
const requests = []
|
|
373
|
+
const revealTimers = []
|
|
268
374
|
const completionTimers = []
|
|
269
375
|
const nativeSetTimeout = dom.window.setTimeout.bind(dom.window)
|
|
270
376
|
const nativeClearTimeout = dom.window.clearTimeout.bind(dom.window)
|
|
271
377
|
dom.window.setTimeout = (callback, delay, ...args) => {
|
|
378
|
+
if (delay <= 500 && delay > 400) {
|
|
379
|
+
const timer = {
|
|
380
|
+
callback,
|
|
381
|
+
delay,
|
|
382
|
+
cleared: false,
|
|
383
|
+
id: 9000 + revealTimers.length
|
|
384
|
+
}
|
|
385
|
+
revealTimers.push(timer)
|
|
386
|
+
return timer.id
|
|
387
|
+
}
|
|
272
388
|
if (delay <= 4000 && delay > 3500) {
|
|
273
389
|
const timer = {
|
|
274
390
|
callback,
|
|
@@ -282,7 +398,8 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
282
398
|
return nativeSetTimeout(callback, delay, ...args)
|
|
283
399
|
}
|
|
284
400
|
dom.window.clearTimeout = (id) => {
|
|
285
|
-
const timer = completionTimers
|
|
401
|
+
const timer = [...revealTimers, ...completionTimers]
|
|
402
|
+
.find((candidate) => candidate.id === id)
|
|
286
403
|
if (timer) {
|
|
287
404
|
timer.cleared = true
|
|
288
405
|
return
|
|
@@ -375,15 +492,37 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
375
492
|
}
|
|
376
493
|
})
|
|
377
494
|
|
|
495
|
+
const checking = [...tray.querySelectorAll(".vault-auto-scan-row")]
|
|
496
|
+
.find((card) => card.querySelector(
|
|
497
|
+
".vault-auto-scan-app").textContent === "ComfyUI")
|
|
498
|
+
assert.ok(checking)
|
|
499
|
+
assert.equal(checking.dataset.state, "checking")
|
|
500
|
+
assert.equal(checking.querySelectorAll(
|
|
501
|
+
".vault-auto-scan-message").length, 1)
|
|
502
|
+
assert.equal(tray.querySelector(
|
|
503
|
+
'.vault-auto-scan-row[data-state="complete"]'), null,
|
|
504
|
+
"the checking phase remains visible before an immediate result")
|
|
505
|
+
assert.equal(revealTimers.length, 1)
|
|
506
|
+
assert.ok(revealTimers[0].delay <= 500)
|
|
507
|
+
|
|
508
|
+
revealTimers[0].callback()
|
|
378
509
|
const completed = tray.querySelector(
|
|
379
510
|
'.vault-auto-scan-row[data-state="complete"]')
|
|
380
511
|
assert.ok(completed)
|
|
381
512
|
assert.equal(completed.querySelector(
|
|
382
513
|
".vault-auto-scan-app").textContent, "ComfyUI")
|
|
514
|
+
assert.deepEqual([...completed.querySelectorAll(
|
|
515
|
+
".vault-auto-scan-status")].map((status) => status.textContent), [
|
|
516
|
+
"Checking for possible duplicate files...",
|
|
517
|
+
"No possible duplicate files found"
|
|
518
|
+
])
|
|
519
|
+
assert.equal(completed.querySelectorAll(
|
|
520
|
+
".vault-auto-scan-message").length, 2)
|
|
383
521
|
assert.equal(completed.querySelector(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
assert.ok(completed.querySelector(
|
|
522
|
+
'.vault-auto-scan-message[data-current="false"]')
|
|
523
|
+
.dataset.state, "checking")
|
|
524
|
+
assert.ok(completed.querySelector(
|
|
525
|
+
'.vault-auto-scan-message[data-current="true"] .vault-auto-scan-icon svg'))
|
|
387
526
|
assert.equal(completed.querySelector(".vault-auto-scan-settings"), null)
|
|
388
527
|
assert.equal(completed.querySelector(".vault-auto-scan-action"), null)
|
|
389
528
|
assert.equal(completionTimers[0].delay, 4000)
|
|
@@ -426,6 +565,10 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
426
565
|
}],
|
|
427
566
|
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
428
567
|
})
|
|
568
|
+
const focusedClose = tray.querySelector(".vault-auto-scan-close")
|
|
569
|
+
focusedClose.focus()
|
|
570
|
+
const revealsBeforeFocusedCompletion = revealTimers.length
|
|
571
|
+
const timersBeforeFocusedCompletion = completionTimers.length
|
|
429
572
|
send({
|
|
430
573
|
enabled: true,
|
|
431
574
|
rows: [],
|
|
@@ -436,6 +579,17 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
436
579
|
}
|
|
437
580
|
})
|
|
438
581
|
assert.equal(tray.hidden, false)
|
|
582
|
+
assert.equal(revealTimers.length, revealsBeforeFocusedCompletion + 1)
|
|
583
|
+
assert.equal(completionTimers.length, timersBeforeFocusedCompletion,
|
|
584
|
+
"completion dismissal cannot start before its result is revealed")
|
|
585
|
+
revealTimers.at(-1).callback()
|
|
586
|
+
assert.equal(completionTimers.length, timersBeforeFocusedCompletion,
|
|
587
|
+
"completion does not start its timer while focus is already in the card")
|
|
588
|
+
focusedClose.dispatchEvent(new dom.window.FocusEvent("focusout", {
|
|
589
|
+
bubbles: true,
|
|
590
|
+
relatedTarget: null
|
|
591
|
+
}))
|
|
592
|
+
assert.equal(completionTimers.length, timersBeforeFocusedCompletion + 1)
|
|
439
593
|
eventSources[0].onerror()
|
|
440
594
|
await new Promise((resolve) => nativeSetTimeout(resolve, 0))
|
|
441
595
|
assert.equal(tray.hidden, true,
|
|
@@ -614,6 +768,7 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
|
|
|
614
768
|
assert.equal(label.textContent, "Auto")
|
|
615
769
|
assert.match(template, /data-app-vault-mode/)
|
|
616
770
|
assert.match(template, /app-vault-mode\.js/)
|
|
771
|
+
assert.doesNotMatch(template, /vault-auto-settings/)
|
|
617
772
|
assert.match(template,
|
|
618
773
|
/#save-space-tab\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*none;/s)
|
|
619
774
|
assert.match(template,
|