pinokiod 8.0.60 → 8.0.62

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.
@@ -10,7 +10,7 @@ const {
10
10
  } = require("./operation_errors")
11
11
 
12
12
  const COMPLETE_PHASES = new Set(["complete", "completed_with_exclusions"])
13
- const STOP_SETTLE_MS = 3000
13
+ const STOP_SETTLE_MS = 1000
14
14
  const PROGRESS_INTERVAL_MS = 5000
15
15
  const isMissing = (error) => !!(error &&
16
16
  (error.code === "ENOENT" || error.code === "ENOTDIR"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.60",
3
+ "version": "8.0.62",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
  }
@@ -874,21 +856,23 @@
874
856
  const COMPLETION_VISIBLE_MS = 4000;
875
857
 
876
858
  let eventSource = null;
877
- let visibleCheckingApps = new Set();
878
- const completions = new Map();
879
-
880
- function statusText(row) {
881
- if (row.state === 'paused') {
882
- return 'Automatic checks are off';
859
+ const cards = new Map();
860
+
861
+ function statusText(state) {
862
+ if (state === 'paused') return 'Automatic checks are paused';
863
+ if (state === 'result') return 'Possible duplicate files found';
864
+ if (state === 'complete') return 'No possible duplicate files found';
865
+ if (state === 'checking-again') {
866
+ return 'Checking again for possible duplicate files...';
883
867
  }
884
- return 'Checking for possible duplicate files';
868
+ return 'Checking for possible duplicate files...';
885
869
  }
886
870
 
887
- function actionFor(row) {
888
- if (row.state === 'paused') {
871
+ function actionFor(state) {
872
+ if (state === 'paused') {
889
873
  return { label: 'Resume', action: 'automatic_resume' };
890
874
  }
891
- if (row.state === 'result') {
875
+ if (state === 'result') {
892
876
  return { label: 'Review', action: 'automatic_review' };
893
877
  }
894
878
  return { label: 'Pause', action: 'automatic_pause' };
@@ -915,21 +899,27 @@
915
899
  return result || {};
916
900
  }
917
901
 
918
- function removeRow(item) {
919
- item.remove();
920
- tray.hidden = tray.childElementCount === 0;
921
- }
922
-
923
- function removeCompletion(app) {
924
- const completion = completions.get(app);
902
+ function stopCompletion(card) {
903
+ const completion = card && card.completion;
925
904
  if (!completion) return;
926
905
  if (completion.timer) window.clearTimeout(completion.timer);
927
- completions.delete(app);
928
- removeRow(completion.item);
906
+ card.completion = null;
929
907
  }
930
908
 
931
- function clearCompletions() {
932
- [...completions.keys()].forEach(removeCompletion);
909
+ function removeCard(app) {
910
+ const card = cards.get(app);
911
+ if (card) {
912
+ stopCompletion(card);
913
+ card.item.remove();
914
+ }
915
+ cards.delete(app);
916
+ tray.hidden = cards.size === 0;
917
+ }
918
+
919
+ function resetCards() {
920
+ [...cards.keys()].forEach(removeCard);
921
+ tray.replaceChildren();
922
+ tray.hidden = true;
933
923
  }
934
924
 
935
925
  function scheduleCompletion(completion) {
@@ -937,11 +927,12 @@
937
927
  completion.startedAt = Date.now();
938
928
  completion.timer = window.setTimeout(() => {
939
929
  completion.timer = null;
940
- removeCompletion(completion.app);
930
+ removeCard(completion.app);
941
931
  }, completion.remaining);
942
932
  }
943
933
 
944
934
  function setCompletionPaused(completion, reason, paused) {
935
+ if (!completion) return;
945
936
  if (paused) {
946
937
  if (completion.paused.has(reason)) return;
947
938
  if (!completion.paused.size && completion.timer) {
@@ -957,60 +948,6 @@
957
948
  if (!completion.paused.size) scheduleCompletion(completion);
958
949
  }
959
950
 
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
951
  function automaticSettingsKey(app) {
1015
952
  return `pinokio:vault:auto-settings:${encodeURIComponent(app)}`;
1016
953
  }
@@ -1024,24 +961,6 @@
1024
961
  try {
1025
962
  sessionStorage.setItem(storageKey, '1');
1026
963
  } 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
964
  const opened = navigateActiveLeaf(href);
1046
965
  if (!opened) {
1047
966
  try { sessionStorage.removeItem(storageKey); } catch (_) {}
@@ -1067,166 +986,271 @@
1067
986
  return opened;
1068
987
  }
1069
988
 
989
+ function createCard(app) {
990
+ const item = document.createElement('section');
991
+ item.className = 'vault-auto-scan-row';
992
+
993
+ const close = document.createElement('button');
994
+ close.type = 'button';
995
+ close.className = 'vault-auto-scan-close';
996
+ close.setAttribute('aria-label',
997
+ `Dismiss Disk Saver notification for ${app}`);
998
+ close.title = 'Dismiss';
999
+ close.textContent = '×';
1000
+
1001
+ const header = document.createElement('header');
1002
+ header.className = 'vault-auto-scan-header';
1003
+ const product = document.createElement('span');
1004
+ product.className = 'vault-auto-scan-product';
1005
+ product.textContent = 'Disk Saver';
1006
+ const separator = document.createElement('span');
1007
+ separator.className = 'vault-auto-scan-separator';
1008
+ separator.setAttribute('aria-hidden', 'true');
1009
+ separator.textContent = '·';
1010
+ const appName = document.createElement('span');
1011
+ appName.className = 'vault-auto-scan-app';
1012
+ appName.textContent = app;
1013
+ appName.title = app;
1014
+ header.append(product, separator, appName);
1015
+
1016
+ const messages = document.createElement('div');
1017
+ messages.className = 'vault-auto-scan-messages';
1018
+ const controls = document.createElement('div');
1019
+ controls.className = 'vault-auto-scan-controls';
1020
+ item.append(close, header, messages, controls);
1021
+
1022
+ const card = {
1023
+ app,
1024
+ item,
1025
+ messages,
1026
+ controls,
1027
+ currentState: null,
1028
+ currentNoticeId: null,
1029
+ dismissPending: false,
1030
+ pointerInside: false,
1031
+ focusInside: false,
1032
+ completion: null
1033
+ };
1034
+ close.addEventListener('click', async () => {
1035
+ if (card.currentState === 'complete') {
1036
+ removeCard(app);
1037
+ return;
1038
+ }
1039
+ card.dismissPending = card.currentState === 'checking';
1040
+ close.disabled = true;
1041
+ try {
1042
+ const result = await requestAction(
1043
+ 'automatic_dismiss', app, card.currentNoticeId);
1044
+ if (result.stale) {
1045
+ card.dismissPending = false;
1046
+ await loadState();
1047
+ if (close.isConnected) close.disabled = false;
1048
+ return;
1049
+ }
1050
+ removeCard(app);
1051
+ } catch (error) {
1052
+ console.warn('[Disk Saver] Automatic check dismissal failed', error);
1053
+ card.dismissPending = false;
1054
+ close.disabled = false;
1055
+ }
1056
+ });
1057
+ item.addEventListener('mouseenter', () => {
1058
+ card.pointerInside = true;
1059
+ setCompletionPaused(card.completion, 'pointer', true);
1060
+ });
1061
+ item.addEventListener('mouseleave', () => {
1062
+ card.pointerInside = false;
1063
+ setCompletionPaused(card.completion, 'pointer', false);
1064
+ });
1065
+ item.addEventListener('focusin', () => {
1066
+ card.focusInside = true;
1067
+ setCompletionPaused(card.completion, 'focus', true);
1068
+ });
1069
+ item.addEventListener('focusout', (event) => {
1070
+ if (!item.contains(event.relatedTarget)) {
1071
+ card.focusInside = false;
1072
+ setCompletionPaused(card.completion, 'focus', false);
1073
+ }
1074
+ });
1075
+ cards.set(app, card);
1076
+ return card;
1077
+ }
1078
+
1079
+ function messageIcon(state) {
1080
+ if (state === 'paused') return PAUSE_ICON;
1081
+ if (state === 'result') return DRIVE_ICON;
1082
+ if (state === 'complete') return COMPLETE_ICON;
1083
+ return '';
1084
+ }
1085
+
1086
+ function appendMessage(card, state) {
1087
+ const previous = card.messages.querySelector('[data-current="true"]');
1088
+ if (previous) {
1089
+ previous.dataset.current = 'false';
1090
+ previous.removeAttribute('aria-current');
1091
+ }
1092
+ const message = document.createElement('div');
1093
+ message.className = 'vault-auto-scan-message';
1094
+ message.dataset.state = state === 'checking-again' ? 'checking' : state;
1095
+ message.dataset.current = 'true';
1096
+ message.setAttribute('aria-current', 'true');
1097
+ const icon = document.createElement('span');
1098
+ icon.className = 'vault-auto-scan-icon';
1099
+ icon.innerHTML = messageIcon(state);
1100
+ const status = document.createElement('span');
1101
+ status.className = 'vault-auto-scan-status';
1102
+ status.textContent = statusText(state);
1103
+ message.append(icon, status);
1104
+ card.messages.appendChild(message);
1105
+ }
1106
+
1107
+ function addSettingsButton(card) {
1108
+ const settings = document.createElement('button');
1109
+ settings.type = 'button';
1110
+ settings.className = 'vault-auto-scan-settings';
1111
+ settings.textContent = 'Automatic check settings';
1112
+ settings.addEventListener('click', async () => {
1113
+ settings.disabled = true;
1114
+ try {
1115
+ const result = await requestAction(
1116
+ 'automatic_settings', card.app);
1117
+ if (!result.href || !openAutomaticSettings(card.app, result.href)) {
1118
+ throw new Error('The app page is unavailable.');
1119
+ }
1120
+ } catch (error) {
1121
+ console.warn('[Disk Saver] Automatic check settings failed', error);
1122
+ } finally {
1123
+ if (settings.isConnected) settings.disabled = false;
1124
+ }
1125
+ });
1126
+ card.controls.appendChild(settings);
1127
+ }
1128
+
1129
+ function addStateAction(card, row) {
1130
+ const action = actionFor(row.state);
1131
+ const button = document.createElement('button');
1132
+ button.type = 'button';
1133
+ button.className = 'vault-auto-scan-action';
1134
+ button.textContent = action.label;
1135
+ button.addEventListener('click', async () => {
1136
+ button.disabled = true;
1137
+ try {
1138
+ const result = await requestAction(
1139
+ action.action, card.app, row.notice_id);
1140
+ if (result.stale) {
1141
+ await loadState();
1142
+ if (button.isConnected) button.disabled = false;
1143
+ return;
1144
+ }
1145
+ if (action.action === 'automatic_review' && result.href) {
1146
+ if (!openAutomaticReview(card.app, result.href)) {
1147
+ throw new Error('The app page could not be opened.');
1148
+ }
1149
+ removeCard(card.app);
1150
+ } else {
1151
+ await loadState();
1152
+ }
1153
+ } catch (error) {
1154
+ console.warn('[Disk Saver] Automatic check action failed', error);
1155
+ if (button.isConnected) button.disabled = false;
1156
+ }
1157
+ });
1158
+ card.controls.appendChild(button);
1159
+ }
1160
+
1161
+ function renderControls(card, row) {
1162
+ card.controls.replaceChildren();
1163
+ if (row.state === 'complete') return;
1164
+ if (row.state !== 'result') addSettingsButton(card);
1165
+ addStateAction(card, row);
1166
+ }
1167
+
1168
+ function updateCard(row) {
1169
+ let card = cards.get(row.app);
1170
+ if (!card) card = createCard(row.app);
1171
+ const unchanged = card.currentState === row.state &&
1172
+ card.currentNoticeId === (row.notice_id || null);
1173
+ if (unchanged) return card;
1174
+ stopCompletion(card);
1175
+ const displayState = row.state === 'checking' &&
1176
+ card.currentState === 'paused' ? 'checking-again' : row.state;
1177
+ appendMessage(card, displayState);
1178
+ card.currentState = row.state;
1179
+ card.currentNoticeId = row.notice_id || null;
1180
+ card.dismissPending = false;
1181
+ card.item.dataset.state = row.state;
1182
+ renderControls(card, row);
1183
+ return card;
1184
+ }
1185
+
1186
+ function startCompletion(app) {
1187
+ const card = cards.get(app);
1188
+ if (!card || card.currentState !== 'checking' ||
1189
+ card.dismissPending || card.completion) return false;
1190
+ appendMessage(card, 'complete');
1191
+ card.currentState = 'complete';
1192
+ card.currentNoticeId = null;
1193
+ card.item.dataset.state = 'complete';
1194
+ renderControls(card, { state: 'complete' });
1195
+ const completion = {
1196
+ app,
1197
+ timer: null,
1198
+ remaining: COMPLETION_VISIBLE_MS,
1199
+ startedAt: 0,
1200
+ paused: new Set()
1201
+ };
1202
+ if (card.pointerInside) completion.paused.add('pointer');
1203
+ if (card.focusInside) completion.paused.add('focus');
1204
+ card.completion = completion;
1205
+ scheduleCompletion(completion);
1206
+ return true;
1207
+ }
1208
+
1070
1209
  function render(snapshot, options = {}) {
1071
1210
  if (!snapshot || snapshot.global_scan_ready !== true) {
1072
- [...completions.keys()].forEach(removeCompletion);
1073
- visibleCheckingApps = new Set();
1074
- tray.replaceChildren();
1075
- tray.hidden = true;
1211
+ resetCards();
1076
1212
  return;
1077
1213
  }
1078
- const rows = snapshot && Array.isArray(snapshot.rows)
1214
+ const rows = Array.isArray(snapshot.rows)
1079
1215
  ? snapshot.rows
1080
1216
  : [];
1081
1217
  const validRows = rows.filter((row) =>
1082
1218
  row && typeof row.app === 'string' && row.app);
1083
1219
  const liveApps = new Set(validRows.map((row) => row.app));
1084
- [...completions.keys()].forEach((app) => {
1085
- if (liveApps.has(app)) removeCompletion(app);
1086
- });
1087
- const manualApps = new Set(snapshot && Array.isArray(snapshot.settings)
1220
+ const manualApps = new Set(Array.isArray(snapshot.settings)
1088
1221
  ? snapshot.settings.filter((setting) =>
1089
1222
  setting && setting.mode === 'manual').map((setting) => setting.app)
1090
1223
  : []);
1091
- manualApps.forEach(removeCompletion);
1092
-
1093
- const completion = snapshot && snapshot.completion;
1224
+ const retained = new Set();
1225
+ validRows.forEach((row) => {
1226
+ updateCard(row);
1227
+ retained.add(row.app);
1228
+ });
1229
+ const completion = snapshot.completion;
1094
1230
  if (options.acceptCompletion && completion &&
1095
1231
  completion.outcome === 'no_possible_duplicates' &&
1096
1232
  typeof completion.app === 'string' && completion.app &&
1097
- visibleCheckingApps.has(completion.app) &&
1098
1233
  !liveApps.has(completion.app) &&
1099
- !manualApps.has(completion.app)) {
1100
- startCompletion(completion.app);
1234
+ !manualApps.has(completion.app) &&
1235
+ startCompletion(completion.app)) {
1236
+ retained.add(completion.app);
1101
1237
  }
1102
-
1103
- const fragment = document.createDocumentFragment();
1238
+ cards.forEach((card, app) => {
1239
+ if (card.completion && !manualApps.has(app)) retained.add(app);
1240
+ });
1241
+ [...cards.keys()].forEach((app) => {
1242
+ if (!retained.has(app)) removeCard(app);
1243
+ });
1104
1244
  validRows.forEach((row) => {
1105
- const item = document.createElement('div');
1106
- item.className = 'vault-auto-scan-row';
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);
1245
+ const card = cards.get(row.app);
1246
+ if (card) tray.appendChild(card.item);
1219
1247
  });
1220
- completions.forEach((completionRow) => {
1221
- if (!liveApps.has(completionRow.app)) {
1222
- fragment.appendChild(completionRow.item);
1248
+ cards.forEach((card, app) => {
1249
+ if (card.completion && !liveApps.has(app)) {
1250
+ tray.appendChild(card.item);
1223
1251
  }
1224
1252
  });
1225
- tray.replaceChildren(fragment);
1226
- tray.hidden = tray.childElementCount === 0;
1227
- visibleCheckingApps = new Set(validRows
1228
- .filter((row) => row.state === 'checking')
1229
- .map((row) => row.app));
1253
+ tray.hidden = cards.size === 0;
1230
1254
  }
1231
1255
 
1232
1256
  async function loadState() {
@@ -1262,7 +1286,7 @@
1262
1286
  }
1263
1287
  };
1264
1288
  eventSource.onerror = () => {
1265
- clearCompletions();
1289
+ resetCards();
1266
1290
  loadState();
1267
1291
  };
1268
1292
  }