@dirwell/theme-ledger 0.0.1-alpha.1 → 0.0.2
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/README.md +34 -2
- package/dist/client.js +27 -28
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +626 -83
- package/dist/index.mjs.map +1 -1
- package/dist/model-BgCmoksr.d.mts +167 -0
- package/dist/model.d.mts +1 -1
- package/dist/source/Ledger.svelte +493 -156
- package/dist/source/component.d.mts +1 -1
- package/dist/source/shared/customization.mjs +255 -0
- package/dist/source/shared/live-search.mjs +28 -0
- package/dist/source/shared/navigation.mjs +197 -2
- package/dist/source/shared/view-state.mjs +128 -0
- package/dist/styles.css +6 -0
- package/package.json +7 -3
- package/dist/model-K7UzS9YB.d.mts +0 -46
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { onMount, tick, untrack } from "svelte";
|
|
3
|
+
import { queryLiveArchive } from "./shared/live-search.mjs";
|
|
4
|
+
import {
|
|
5
|
+
readView,
|
|
6
|
+
createViewHistory,
|
|
7
|
+
} from "./shared/view-state.mjs";
|
|
3
8
|
import { entryIcon } from "./shared/icons.mjs";
|
|
4
9
|
import { loadListing } from "@dirwell/core/client";
|
|
5
10
|
import {
|
|
@@ -12,10 +17,17 @@
|
|
|
12
17
|
directoryLike,
|
|
13
18
|
entryType,
|
|
14
19
|
size,
|
|
15
|
-
modified,
|
|
16
20
|
fuzzy,
|
|
17
21
|
} from "./shared/records.mjs";
|
|
18
|
-
|
|
22
|
+
import {
|
|
23
|
+
resolveCustomization,
|
|
24
|
+
label,
|
|
25
|
+
formattedDate,
|
|
26
|
+
copyEntryAction,
|
|
27
|
+
columnLabel,
|
|
28
|
+
publicDataValue,
|
|
29
|
+
} from "./shared/customization.mjs";
|
|
30
|
+
let { model: initialModel, header, row, actions, footer } = $props();
|
|
19
31
|
const snapshot = untrack(() =>
|
|
20
32
|
restoreThemePage(initialModel, Boolean(import.meta.hot)),
|
|
21
33
|
);
|
|
@@ -29,14 +41,26 @@
|
|
|
29
41
|
ready = $state(false),
|
|
30
42
|
busy = $state(false),
|
|
31
43
|
error = $state(""),
|
|
44
|
+
failure = $state(null),
|
|
32
45
|
timestamp = $state(null),
|
|
33
46
|
composing = $state(false),
|
|
34
47
|
scrollTop = $state(0),
|
|
48
|
+
viewportHeight = $state(800),
|
|
35
49
|
globalOpen = $state(false),
|
|
36
50
|
globalQuery = $state(""),
|
|
37
51
|
globalRows = $state.raw([]),
|
|
38
|
-
globalStatus = $state(
|
|
52
|
+
globalStatus = $state(
|
|
53
|
+
resolveCustomization(snapshot.ui).labels.beginArchive,
|
|
54
|
+
),
|
|
39
55
|
pendingUpdates = $state(null);
|
|
56
|
+
let ui = $derived(resolveCustomization(model.ui));
|
|
57
|
+
let archiveFailed = $state(false);
|
|
58
|
+
let countParts = $derived(
|
|
59
|
+
label(ui, complete ? "entries" : "pageEntries", {
|
|
60
|
+
count: "__COUNT__",
|
|
61
|
+
total: model.total,
|
|
62
|
+
}).split("__COUNT__"),
|
|
63
|
+
);
|
|
40
64
|
let heights = $state.raw(new Map());
|
|
41
65
|
let updatesHref = $derived(
|
|
42
66
|
ready && model.updatesHref
|
|
@@ -65,7 +89,17 @@
|
|
|
65
89
|
}
|
|
66
90
|
return Math.max(0, low - 5);
|
|
67
91
|
}),
|
|
68
|
-
end = $derived
|
|
92
|
+
end = $derived.by(() => {
|
|
93
|
+
if (!virtual) return rows.length;
|
|
94
|
+
let low = 0,
|
|
95
|
+
high = rows.length;
|
|
96
|
+
while (low < high) {
|
|
97
|
+
const middle = (low + high) >> 1;
|
|
98
|
+
if (offsets[middle + 1] <= scrollTop + viewportHeight) low = middle + 1;
|
|
99
|
+
else high = middle;
|
|
100
|
+
}
|
|
101
|
+
return Math.min(rows.length, Math.max(start + 35, low + 5));
|
|
102
|
+
});
|
|
69
103
|
function measure(node, id) {
|
|
70
104
|
if (typeof ResizeObserver === "undefined") return;
|
|
71
105
|
const observer = new ResizeObserver(() => {
|
|
@@ -87,7 +121,53 @@
|
|
|
87
121
|
let navigation = 0,
|
|
88
122
|
abort,
|
|
89
123
|
archiveAbort,
|
|
90
|
-
archiveRequest = 0
|
|
124
|
+
archiveRequest = 0,
|
|
125
|
+
archiveCache = null,
|
|
126
|
+
archiveCacheHref = "",
|
|
127
|
+
archiveCacheAt = 0,
|
|
128
|
+
viewHistory,
|
|
129
|
+
retryRequest = null;
|
|
130
|
+
const viewDefaults = (page) => ({
|
|
131
|
+
query: "",
|
|
132
|
+
sort: page.sort,
|
|
133
|
+
types: ["directory", "file", "link"],
|
|
134
|
+
});
|
|
135
|
+
function saveView() {
|
|
136
|
+
if (!viewHistory) return;
|
|
137
|
+
viewHistory.save(
|
|
138
|
+
new URL(model.dataHref, document.baseURI).href,
|
|
139
|
+
model.path,
|
|
140
|
+
{
|
|
141
|
+
query,
|
|
142
|
+
types,
|
|
143
|
+
sort,
|
|
144
|
+
selected,
|
|
145
|
+
windowY: window.scrollY,
|
|
146
|
+
listTop: scrollTop,
|
|
147
|
+
},
|
|
148
|
+
viewDefaults(model),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
function retry() {
|
|
152
|
+
if (failure === "folder" && retryRequest) openFolder(...retryRequest, true);
|
|
153
|
+
else openFolder(model.dataHref, location.href, false, true);
|
|
154
|
+
}
|
|
155
|
+
$effect(() => {
|
|
156
|
+
if (!ready || busy) return;
|
|
157
|
+
const view = { query, types, sort, selected, listTop: scrollTop };
|
|
158
|
+
const dataHref = model.dataHref,
|
|
159
|
+
path = model.path,
|
|
160
|
+
defaults = viewDefaults(model);
|
|
161
|
+
const frame = requestAnimationFrame(() =>
|
|
162
|
+
viewHistory?.save(
|
|
163
|
+
new URL(dataHref, document.baseURI).href,
|
|
164
|
+
path,
|
|
165
|
+
{ ...view, windowY: window.scrollY },
|
|
166
|
+
defaults,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
return () => cancelAnimationFrame(frame);
|
|
170
|
+
});
|
|
91
171
|
const isDirectory = directoryLike;
|
|
92
172
|
function safeHref(record) {
|
|
93
173
|
if (
|
|
@@ -117,14 +197,16 @@
|
|
|
117
197
|
if (token === navigation && !local.signal.aborted)
|
|
118
198
|
entries = loaded.entries;
|
|
119
199
|
} catch (cause) {
|
|
120
|
-
if (token === navigation && !local.signal.aborted)
|
|
121
|
-
error =
|
|
122
|
-
|
|
200
|
+
if (token === navigation && !local.signal.aborted) {
|
|
201
|
+
error = ui.labels.listingError;
|
|
202
|
+
failure = "listing";
|
|
203
|
+
}
|
|
123
204
|
} finally {
|
|
124
205
|
if (token === navigation) busy = false;
|
|
125
206
|
}
|
|
126
207
|
}
|
|
127
208
|
async function navigate(event, record) {
|
|
209
|
+
saveView();
|
|
128
210
|
const dataHref = record.presentation?.folderDataHref;
|
|
129
211
|
if (
|
|
130
212
|
record.exitsExplorer ||
|
|
@@ -144,19 +226,28 @@
|
|
|
144
226
|
true,
|
|
145
227
|
);
|
|
146
228
|
}
|
|
147
|
-
async function openFolder(
|
|
229
|
+
async function openFolder(
|
|
230
|
+
dataHref,
|
|
231
|
+
url,
|
|
232
|
+
push,
|
|
233
|
+
refresh = false,
|
|
234
|
+
restoreState = null,
|
|
235
|
+
fresh = false,
|
|
236
|
+
) {
|
|
237
|
+
if (push) saveView();
|
|
148
238
|
const token = ++navigation;
|
|
149
239
|
abort?.abort();
|
|
150
240
|
const local = (abort = new AbortController());
|
|
151
241
|
busy = true;
|
|
152
242
|
error = "";
|
|
243
|
+
failure = null;
|
|
153
244
|
try {
|
|
154
245
|
const next = await fetchThemePage(
|
|
155
246
|
dataHref,
|
|
156
247
|
url,
|
|
157
248
|
"ledger",
|
|
158
249
|
local.signal,
|
|
159
|
-
refresh,
|
|
250
|
+
refresh || fresh,
|
|
160
251
|
);
|
|
161
252
|
let nextEntries = next.entries;
|
|
162
253
|
if (refresh && next.manifestHref && nextEntries.length !== next.total) {
|
|
@@ -168,27 +259,60 @@
|
|
|
168
259
|
).entries;
|
|
169
260
|
}
|
|
170
261
|
if (token !== navigation || local.signal.aborted) return;
|
|
262
|
+
if (!refresh) viewHistory.commit(next.dataHref, next.path, url, push);
|
|
171
263
|
model = next;
|
|
172
264
|
entries = nextEntries;
|
|
265
|
+
retryRequest = null;
|
|
173
266
|
if (!refresh) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
267
|
+
const view = readView(
|
|
268
|
+
url,
|
|
269
|
+
"ledger",
|
|
270
|
+
next.path,
|
|
271
|
+
viewDefaults(next),
|
|
272
|
+
[],
|
|
273
|
+
restoreState,
|
|
274
|
+
);
|
|
275
|
+
query = view.query;
|
|
276
|
+
types = view.types;
|
|
277
|
+
sort = view.sort;
|
|
278
|
+
selected = view.selected;
|
|
279
|
+
scrollTop = view.listTop;
|
|
177
280
|
}
|
|
178
|
-
if (push) history.pushState({ dataHref: next.dataHref }, "", url);
|
|
179
281
|
applyThemeMetadata(next, url);
|
|
180
282
|
await hydrateData();
|
|
181
|
-
if (!refresh)
|
|
283
|
+
if (!refresh) {
|
|
284
|
+
const view = readView(
|
|
285
|
+
url,
|
|
286
|
+
"ledger",
|
|
287
|
+
next.path,
|
|
288
|
+
viewDefaults(next),
|
|
289
|
+
[],
|
|
290
|
+
restoreState,
|
|
291
|
+
);
|
|
292
|
+
await tick();
|
|
293
|
+
if (selected && !rows.some((entry) => entry.id === selected))
|
|
294
|
+
selected = null;
|
|
295
|
+
window.scrollTo({ top: view.windowY });
|
|
296
|
+
if (restoreState && selected)
|
|
297
|
+
list
|
|
298
|
+
?.querySelector(`[data-id="${CSS.escape(selected)}"] a`)
|
|
299
|
+
?.focus({ preventScroll: true });
|
|
300
|
+
}
|
|
182
301
|
} catch (cause) {
|
|
183
|
-
if (token === navigation && !local.signal.aborted)
|
|
184
|
-
error =
|
|
185
|
-
|
|
302
|
+
if (token === navigation && !local.signal.aborted) {
|
|
303
|
+
error = ui.labels.folderError;
|
|
304
|
+
failure = "folder";
|
|
305
|
+
retryRequest = [dataHref, url, push, refresh, restoreState];
|
|
306
|
+
viewHistory?.rollback();
|
|
307
|
+
}
|
|
186
308
|
} finally {
|
|
187
309
|
if (token === navigation) busy = false;
|
|
188
310
|
}
|
|
189
311
|
}
|
|
190
312
|
function navigateLink(event, href, dataHref) {
|
|
191
313
|
if (
|
|
314
|
+
typeof href !== "string" ||
|
|
315
|
+
typeof dataHref !== "string" ||
|
|
192
316
|
event?.altKey ||
|
|
193
317
|
event?.ctrlKey ||
|
|
194
318
|
event?.metaKey ||
|
|
@@ -201,11 +325,12 @@
|
|
|
201
325
|
openFolder(dataHref, new URL(href, document.baseURI).href, true);
|
|
202
326
|
}
|
|
203
327
|
async function move(index) {
|
|
204
|
-
const
|
|
328
|
+
const target = Math.max(0, Math.min(rows.length - 1, index));
|
|
329
|
+
const row = rows[target];
|
|
205
330
|
if (!row) return;
|
|
206
331
|
selected = row.id;
|
|
207
332
|
if (virtual) {
|
|
208
|
-
scrollTop = Math.max(0, offsets[
|
|
333
|
+
scrollTop = Math.max(0, offsets[target] - 100);
|
|
209
334
|
if (list)
|
|
210
335
|
window.scrollTo({
|
|
211
336
|
top: list.getBoundingClientRect().top + window.scrollY + scrollTop,
|
|
@@ -222,7 +347,7 @@
|
|
|
222
347
|
if (globalOpen) {
|
|
223
348
|
if (event.key === "Escape") {
|
|
224
349
|
event.preventDefault();
|
|
225
|
-
|
|
350
|
+
closeArchive();
|
|
226
351
|
}
|
|
227
352
|
return;
|
|
228
353
|
}
|
|
@@ -230,6 +355,7 @@
|
|
|
230
355
|
if (event.key === "Escape") timestamp = null;
|
|
231
356
|
return;
|
|
232
357
|
}
|
|
358
|
+
if (event.target?.closest?.("[data-dirwell-actions]")) return;
|
|
233
359
|
const editable = /INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "");
|
|
234
360
|
if (event.key === "/" && !editable) {
|
|
235
361
|
event.preventDefault();
|
|
@@ -245,7 +371,11 @@
|
|
|
245
371
|
}
|
|
246
372
|
if (event.key === "Backspace" && model.parentHref) {
|
|
247
373
|
event.preventDefault();
|
|
248
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
location.protocol === "file:" ||
|
|
376
|
+
typeof model.parentDataHref !== "string"
|
|
377
|
+
)
|
|
378
|
+
location.href = model.parentHref;
|
|
249
379
|
else
|
|
250
380
|
openFolder(
|
|
251
381
|
model.parentDataHref,
|
|
@@ -272,59 +402,126 @@
|
|
|
272
402
|
localStorage.setItem("dirwell-color-scheme", value);
|
|
273
403
|
} catch {}
|
|
274
404
|
}
|
|
405
|
+
let archiveDebounce,
|
|
406
|
+
archiveTrigger,
|
|
407
|
+
archivePending = false;
|
|
408
|
+
async function closeArchive() {
|
|
409
|
+
globalOpen = false;
|
|
410
|
+
await tick();
|
|
411
|
+
archiveTrigger?.focus();
|
|
412
|
+
}
|
|
413
|
+
function scheduleArchiveSearch() {
|
|
414
|
+
archiveAbort?.abort();
|
|
415
|
+
archiveRequest++;
|
|
416
|
+
archivePending = false;
|
|
417
|
+
clearTimeout(archiveDebounce);
|
|
418
|
+
archiveDebounce = setTimeout(() => {
|
|
419
|
+
archiveDebounce = undefined;
|
|
420
|
+
searchArchive();
|
|
421
|
+
}, 300);
|
|
422
|
+
}
|
|
423
|
+
$effect(() => {
|
|
424
|
+
if (!globalOpen || !model.searchQueryHref) return;
|
|
425
|
+
const timer = setInterval(() => {
|
|
426
|
+
if (
|
|
427
|
+
document.visibilityState === "visible" &&
|
|
428
|
+
globalQuery.trim() &&
|
|
429
|
+
!archivePending &&
|
|
430
|
+
archiveDebounce === undefined
|
|
431
|
+
)
|
|
432
|
+
searchArchive();
|
|
433
|
+
}, model.searchRefreshIntervalMs ?? 2000);
|
|
434
|
+
return () => {
|
|
435
|
+
clearInterval(timer);
|
|
436
|
+
clearTimeout(archiveDebounce);
|
|
437
|
+
archiveDebounce = undefined;
|
|
438
|
+
archivePending = false;
|
|
439
|
+
archiveAbort?.abort();
|
|
440
|
+
archiveRequest++;
|
|
441
|
+
};
|
|
442
|
+
});
|
|
275
443
|
async function searchArchive() {
|
|
276
444
|
const token = ++archiveRequest;
|
|
277
445
|
archiveAbort?.abort();
|
|
278
446
|
const local = (archiveAbort = new AbortController());
|
|
279
447
|
globalRows = [];
|
|
448
|
+
archiveFailed = false;
|
|
280
449
|
const query = globalQuery.trim();
|
|
281
450
|
if (!query) {
|
|
282
|
-
globalStatus =
|
|
451
|
+
globalStatus = ui.labels.beginArchive;
|
|
283
452
|
return;
|
|
284
453
|
}
|
|
285
|
-
globalStatus =
|
|
454
|
+
globalStatus = ui.labels.searchingArchive;
|
|
455
|
+
archivePending = true;
|
|
286
456
|
try {
|
|
457
|
+
if (model.searchQueryHref) {
|
|
458
|
+
const result = await queryLiveArchive(
|
|
459
|
+
model.searchQueryHref,
|
|
460
|
+
query,
|
|
461
|
+
local.signal,
|
|
462
|
+
);
|
|
463
|
+
if (token !== archiveRequest || local.signal.aborted) return;
|
|
464
|
+
globalRows = result.records;
|
|
465
|
+
globalStatus = result.truncated
|
|
466
|
+
? label(ui, "truncatedResults", {
|
|
467
|
+
shown: result.records.length,
|
|
468
|
+
count: result.records.length + "+",
|
|
469
|
+
})
|
|
470
|
+
: label(ui, "archiveResults", { count: result.records.length });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
287
473
|
const url = new URL(model.searchIndexHref, document.baseURI);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
!
|
|
295
|
-
index
|
|
296
|
-
)
|
|
297
|
-
throw new Error("Unsupported archive");
|
|
298
|
-
const all = [];
|
|
299
|
-
for (const shard of index.shards) {
|
|
300
|
-
if (typeof shard !== "string") throw new Error("Invalid shard");
|
|
301
|
-
const shardUrl = new URL(shard, url);
|
|
302
|
-
if (shardUrl.origin !== url.origin) throw new Error("Unsafe shard");
|
|
303
|
-
const chunk = await fetch(shardUrl, { signal: local.signal });
|
|
304
|
-
if (!chunk.ok) throw new Error("Unavailable shard");
|
|
305
|
-
const data = await chunk.json();
|
|
474
|
+
let all =
|
|
475
|
+
archiveCacheHref === url.href && Date.now() - archiveCacheAt < 60000
|
|
476
|
+
? archiveCache
|
|
477
|
+
: null;
|
|
478
|
+
if (all === null) {
|
|
479
|
+
const response = await fetch(url, { signal: local.signal });
|
|
480
|
+
if (!response.ok) throw new Error("Unavailable archive");
|
|
481
|
+
const index = await response.json();
|
|
306
482
|
if (
|
|
307
|
-
|
|
308
|
-
|
|
483
|
+
index.version !== 3 ||
|
|
484
|
+
!Array.isArray(index.shards) ||
|
|
485
|
+
!Number.isSafeInteger(index.count) ||
|
|
486
|
+
index.count > 100000
|
|
309
487
|
)
|
|
310
|
-
throw new Error("
|
|
311
|
-
|
|
488
|
+
throw new Error("Unsupported archive");
|
|
489
|
+
all = [];
|
|
490
|
+
for (const shard of index.shards) {
|
|
491
|
+
if (typeof shard !== "string") throw new Error("Invalid shard");
|
|
492
|
+
const shardUrl = new URL(shard, url);
|
|
493
|
+
if (shardUrl.origin !== url.origin) throw new Error("Unsafe shard");
|
|
494
|
+
const chunk = await fetch(shardUrl, { signal: local.signal });
|
|
495
|
+
if (!chunk.ok) throw new Error("Unavailable shard");
|
|
496
|
+
const data = await chunk.json();
|
|
312
497
|
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
typeof record.exitsExplorer !== "boolean"
|
|
498
|
+
!Array.isArray(data.entries) ||
|
|
499
|
+
all.length + data.entries.length > 100000
|
|
316
500
|
)
|
|
317
|
-
throw new Error("Invalid archive
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
501
|
+
throw new Error("Invalid archive");
|
|
502
|
+
for (const record of data.entries) {
|
|
503
|
+
if (
|
|
504
|
+
typeof record.name !== "string" ||
|
|
505
|
+
typeof record.path !== "string" ||
|
|
506
|
+
typeof record.exitsExplorer !== "boolean"
|
|
507
|
+
)
|
|
508
|
+
throw new Error("Invalid archive record");
|
|
509
|
+
const href =
|
|
510
|
+
record.href === null ? null : new URL(record.href, url);
|
|
511
|
+
if (href && !["http:", "https:"].includes(href.protocol))
|
|
512
|
+
throw new Error("Unsupported archive link");
|
|
513
|
+
all.push({
|
|
514
|
+
name: record.name,
|
|
515
|
+
path: record.path,
|
|
516
|
+
href: href?.href ?? null,
|
|
517
|
+
exitsExplorer: record.exitsExplorer,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
327
520
|
}
|
|
521
|
+
if (token !== archiveRequest || local.signal.aborted) return;
|
|
522
|
+
archiveCache = all;
|
|
523
|
+
archiveCacheHref = url.href;
|
|
524
|
+
archiveCacheAt = Date.now();
|
|
328
525
|
}
|
|
329
526
|
const matches = all
|
|
330
527
|
.map((record) => ({
|
|
@@ -340,27 +537,49 @@
|
|
|
340
537
|
globalRows = matches.slice(0, 100).map((item) => item.record);
|
|
341
538
|
globalStatus =
|
|
342
539
|
matches.length > 100
|
|
343
|
-
? "
|
|
344
|
-
: matches.length
|
|
540
|
+
? label(ui, "truncatedResults", { shown: 100, count: matches.length })
|
|
541
|
+
: label(ui, "archiveResults", { count: matches.length });
|
|
345
542
|
} catch {
|
|
346
|
-
if (token === archiveRequest && !local.signal.aborted)
|
|
347
|
-
globalStatus =
|
|
543
|
+
if (token === archiveRequest && !local.signal.aborted) {
|
|
544
|
+
globalStatus = ui.labels.archiveError;
|
|
545
|
+
archiveFailed = true;
|
|
546
|
+
}
|
|
547
|
+
} finally {
|
|
548
|
+
if (token === archiveRequest) archivePending = false;
|
|
348
549
|
}
|
|
349
550
|
}
|
|
350
551
|
onMount(() => {
|
|
552
|
+
viewportHeight = innerHeight;
|
|
351
553
|
ready = true;
|
|
352
554
|
abort = new AbortController();
|
|
353
555
|
try {
|
|
354
556
|
const stored = localStorage.getItem("dirwell-color-scheme");
|
|
355
557
|
if (["system", "light", "dark"].includes(stored)) scheme = stored;
|
|
356
558
|
} catch {}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
559
|
+
viewHistory = createViewHistory("ledger");
|
|
560
|
+
const view = readView(
|
|
561
|
+
location.href,
|
|
562
|
+
"ledger",
|
|
563
|
+
model.path,
|
|
564
|
+
viewDefaults(model),
|
|
565
|
+
[],
|
|
566
|
+
history.state,
|
|
360
567
|
);
|
|
568
|
+
query = view.query;
|
|
569
|
+
types = view.types;
|
|
570
|
+
sort = view.sort;
|
|
571
|
+
selected = view.selected;
|
|
572
|
+
scrollTop = view.listTop;
|
|
573
|
+
saveView();
|
|
361
574
|
const onPop = (event) => {
|
|
362
575
|
if (event.state?.dataHref)
|
|
363
|
-
openFolder(
|
|
576
|
+
openFolder(
|
|
577
|
+
event.state.dataHref,
|
|
578
|
+
location.href,
|
|
579
|
+
false,
|
|
580
|
+
false,
|
|
581
|
+
event.state,
|
|
582
|
+
);
|
|
364
583
|
else location.reload();
|
|
365
584
|
};
|
|
366
585
|
window.addEventListener("popstate", onPop);
|
|
@@ -376,10 +595,15 @@
|
|
|
376
595
|
if (!href || typeof EventSource === "undefined") return;
|
|
377
596
|
const updates = new EventSource(href);
|
|
378
597
|
updates.onmessage = () => (pendingUpdates = href);
|
|
379
|
-
updates.onerror = () =>
|
|
380
|
-
|
|
598
|
+
updates.onerror = () => {
|
|
599
|
+
error = ui.labels.updatesError;
|
|
600
|
+
failure = "updates";
|
|
601
|
+
};
|
|
381
602
|
updates.onopen = () => {
|
|
382
|
-
if (
|
|
603
|
+
if (failure === "updates") {
|
|
604
|
+
error = "";
|
|
605
|
+
failure = null;
|
|
606
|
+
}
|
|
383
607
|
};
|
|
384
608
|
return () => updates.close();
|
|
385
609
|
});
|
|
@@ -406,6 +630,7 @@
|
|
|
406
630
|
</script>
|
|
407
631
|
|
|
408
632
|
<svelte:window
|
|
633
|
+
onresize={() => (viewportHeight = innerHeight)}
|
|
409
634
|
onkeydown={keyboard}
|
|
410
635
|
onscroll={() => {
|
|
411
636
|
if (list) scrollTop = Math.max(0, window.scrollY - list.offsetTop);
|
|
@@ -413,14 +638,21 @@
|
|
|
413
638
|
/>
|
|
414
639
|
<header class="page-header">
|
|
415
640
|
<div class="wordmark">
|
|
416
|
-
{#if header}{@render header({ model })}{:else}
|
|
417
|
-
|
|
641
|
+
{#if header}{@render header({ model })}{:else}{#if ui.brand.logoUrl}<img
|
|
642
|
+
src={ui.brand.logoUrl}
|
|
643
|
+
alt={ui.brand.logoAlt ?? ""}
|
|
644
|
+
width="40"
|
|
645
|
+
height="40"
|
|
646
|
+
/>{/if}
|
|
647
|
+
<h1>{ui.brand.name ?? model.title}</h1>
|
|
648
|
+
<span>{ui.labels.archive}</span>{/if}
|
|
418
649
|
</div>
|
|
419
|
-
<nav aria-label=
|
|
650
|
+
<nav aria-label={ui.labels.breadcrumb}>
|
|
420
651
|
<a
|
|
421
652
|
href={model.rootHref}
|
|
422
653
|
onclick={(event) =>
|
|
423
|
-
navigateLink(event, model.rootHref, model.rootDataHref)}
|
|
654
|
+
navigateLink(event, model.rootHref, model.rootDataHref)}
|
|
655
|
+
>{ui.labels.home}</a
|
|
424
656
|
>{#each model.breadcrumbs as crumb}
|
|
425
657
|
/ <a
|
|
426
658
|
href={crumb.href}
|
|
@@ -432,10 +664,10 @@
|
|
|
432
664
|
<main data-explorer data-framework="svelte" aria-busy={busy}>
|
|
433
665
|
<div class="toolbar">
|
|
434
666
|
<label class="search-field"
|
|
435
|
-
>
|
|
667
|
+
>{ui.labels.searchDirectory}<input
|
|
436
668
|
bind:this={searchInput}
|
|
437
669
|
type="search"
|
|
438
|
-
placeholder=
|
|
670
|
+
placeholder={ui.labels.searchFiles}
|
|
439
671
|
bind:value={query}
|
|
440
672
|
disabled={!ready || !complete}
|
|
441
673
|
oncompositionstart={() => (composing = true)}
|
|
@@ -443,7 +675,8 @@
|
|
|
443
675
|
/></label
|
|
444
676
|
>
|
|
445
677
|
<fieldset disabled={!ready || !complete}>
|
|
446
|
-
<legend>
|
|
678
|
+
<legend>{ui.labels.types}</legend
|
|
679
|
+
>{#each ["directory", "file", "link"] as type}<label
|
|
447
680
|
><input
|
|
448
681
|
type="checkbox"
|
|
449
682
|
checked={types.includes(type)}
|
|
@@ -451,38 +684,39 @@
|
|
|
451
684
|
(types = event.currentTarget.checked
|
|
452
685
|
? [...types, type]
|
|
453
686
|
: types.filter((value) => value !== type))}
|
|
454
|
-
/>{type}</label
|
|
687
|
+
/>{ui.labels[type]}</label
|
|
455
688
|
>{/each}
|
|
456
689
|
</fieldset>
|
|
457
690
|
<label
|
|
458
|
-
>
|
|
691
|
+
>{ui.labels.sort}<select
|
|
459
692
|
value={sort.field}
|
|
460
693
|
disabled={!ready || !complete}
|
|
461
694
|
onchange={(event) =>
|
|
462
695
|
(sort = { ...sort, field: event.currentTarget.value })}
|
|
463
|
-
><option value="name">
|
|
464
|
-
|
|
696
|
+
><option value="name">{ui.labels.name}</option><option value="size"
|
|
697
|
+
>{ui.labels.size}</option
|
|
698
|
+
><option value="modified">{ui.labels.modified}</option></select
|
|
465
699
|
></label
|
|
466
700
|
>
|
|
467
701
|
<label
|
|
468
|
-
>
|
|
702
|
+
>{ui.labels.names}<select
|
|
469
703
|
value={sort.nameMode}
|
|
470
704
|
disabled={!ready || !complete}
|
|
471
705
|
onchange={(event) =>
|
|
472
706
|
(sort = { ...sort, nameMode: event.currentTarget.value })}
|
|
473
|
-
><option value="natural">
|
|
474
|
-
>
|
|
475
|
-
><option value="unicode">
|
|
707
|
+
><option value="natural">{ui.labels.natural}</option><option
|
|
708
|
+
value="locale">{ui.labels.locale}</option
|
|
709
|
+
><option value="unicode">{ui.labels.unicode}</option></select
|
|
476
710
|
></label
|
|
477
711
|
>
|
|
478
712
|
<label
|
|
479
|
-
>
|
|
713
|
+
>{ui.labels.order}<select
|
|
480
714
|
value={sort.direction}
|
|
481
715
|
disabled={!ready || !complete}
|
|
482
716
|
onchange={(event) =>
|
|
483
717
|
(sort = { ...sort, direction: event.currentTarget.value })}
|
|
484
|
-
><option value="asc">
|
|
485
|
-
>
|
|
718
|
+
><option value="asc">{ui.labels.ascending}</option><option value="desc"
|
|
719
|
+
>{ui.labels.descending}</option
|
|
486
720
|
></select
|
|
487
721
|
></label
|
|
488
722
|
>
|
|
@@ -493,34 +727,57 @@
|
|
|
493
727
|
disabled={!ready || !complete}
|
|
494
728
|
onchange={(event) =>
|
|
495
729
|
(sort = { ...sort, directoriesFirst: event.currentTarget.checked })}
|
|
496
|
-
/>
|
|
730
|
+
/>{ui.labels.directoriesFirst}</label
|
|
497
731
|
>
|
|
498
732
|
<label
|
|
499
|
-
>
|
|
733
|
+
>{ui.labels.appearance}<select
|
|
500
734
|
value={scheme}
|
|
501
735
|
disabled={!ready}
|
|
502
736
|
onchange={(event) => updateScheme(event.currentTarget.value)}
|
|
503
|
-
><option value="system">
|
|
504
|
-
>
|
|
505
|
-
><option value="dark">
|
|
737
|
+
><option value="system">{ui.labels.system}</option><option value="light"
|
|
738
|
+
>{ui.labels.light}</option
|
|
739
|
+
><option value="dark">{ui.labels.dark}</option></select
|
|
506
740
|
></label
|
|
507
741
|
>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
742
|
+
{#if model.searchIndexHref || model.searchQueryHref}
|
|
743
|
+
<button
|
|
744
|
+
bind:this={archiveTrigger}
|
|
745
|
+
disabled={!ready}
|
|
746
|
+
onclick={() => (globalOpen = true)}>{ui.labels.searchArchive}</button
|
|
747
|
+
>
|
|
748
|
+
{/if}
|
|
512
749
|
</div>
|
|
513
750
|
<section class="listing-card">
|
|
514
751
|
<div class="listing-summary">
|
|
515
|
-
<span data-visible-count
|
|
516
|
-
|
|
517
|
-
|
|
752
|
+
{countParts[0]}<span data-visible-count
|
|
753
|
+
>{complete ? rows.length : entries.length}</span
|
|
754
|
+
>{countParts.slice(1).join("__COUNT__")}<span
|
|
518
755
|
role="status"
|
|
519
|
-
aria-live="polite">{busy ?
|
|
756
|
+
aria-live="polite">{busy ? ui.labels.loading : error}</span
|
|
520
757
|
>
|
|
521
758
|
</div>
|
|
522
|
-
<
|
|
523
|
-
|
|
759
|
+
{#if failure && failure !== "copy"}<button disabled={busy} onclick={retry}
|
|
760
|
+
>{ui.labels.retry}</button
|
|
761
|
+
>{/if}
|
|
762
|
+
{#if failure === "folder" && retryRequest}<a href={retryRequest[1]}
|
|
763
|
+
>{ui.labels.openNative}</a
|
|
764
|
+
>{/if}
|
|
765
|
+
<div
|
|
766
|
+
class="entry-head"
|
|
767
|
+
data-custom-columns={Boolean(model.ui?.columns)}
|
|
768
|
+
style:grid-template-columns={model.ui?.columns
|
|
769
|
+
? ui.columns
|
|
770
|
+
.map((column) =>
|
|
771
|
+
column === "name"
|
|
772
|
+
? "minmax(0,1fr)"
|
|
773
|
+
: typeof column === "object"
|
|
774
|
+
? "minmax(0,12rem)"
|
|
775
|
+
: "minmax(6rem,max-content)",
|
|
776
|
+
)
|
|
777
|
+
.join(" ")
|
|
778
|
+
: undefined}
|
|
779
|
+
>
|
|
780
|
+
{#each ui.columns as column}<span>{columnLabel(column, ui)}</span>{/each}
|
|
524
781
|
</div>
|
|
525
782
|
<ul class="entry-list" bind:this={list}>
|
|
526
783
|
{#if model.parentHref}<li class="entry">
|
|
@@ -540,48 +797,102 @@
|
|
|
540
797
|
class="entry"
|
|
541
798
|
use:measure={entry.id}
|
|
542
799
|
data-entry
|
|
800
|
+
data-custom-columns={Boolean(model.ui?.columns)}
|
|
543
801
|
data-id={entry.id}
|
|
544
802
|
data-order={start + index}
|
|
545
803
|
data-active={selected === entry.id}
|
|
804
|
+
style:grid-template-columns={model.ui?.columns
|
|
805
|
+
? ui.columns
|
|
806
|
+
.map((column) =>
|
|
807
|
+
column === "name"
|
|
808
|
+
? "minmax(0,1fr)"
|
|
809
|
+
: "minmax(6rem,max-content)",
|
|
810
|
+
)
|
|
811
|
+
.join(" ")
|
|
812
|
+
: undefined}
|
|
546
813
|
>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
814
|
+
{#each ui.columns as column}
|
|
815
|
+
{#if column === "name"}<span class="identity"
|
|
816
|
+
>{#if entry.href}<a
|
|
817
|
+
class="name"
|
|
818
|
+
href={safeHref(entry)}
|
|
819
|
+
target={entry.exitsExplorer ? "_blank" : undefined}
|
|
820
|
+
rel={entry.exitsExplorer ? "noopener" : undefined}
|
|
821
|
+
onclick={(event) => navigate(event, entry)}
|
|
822
|
+
onfocus={() => (selected = entry.id)}
|
|
823
|
+
>{#if row}{@render row({
|
|
824
|
+
model,
|
|
825
|
+
entry,
|
|
826
|
+
selected: selected === entry.id,
|
|
827
|
+
})}{:else}<span class="file-icon" aria-hidden="true"
|
|
828
|
+
>{@html entryIcon(entryType(entry))}</span
|
|
829
|
+
>
|
|
830
|
+
{entry.name}{isDirectory(entry) ? "/" : ""}{/if}</a
|
|
831
|
+
>{:else}<span
|
|
832
|
+
>{#if row}{@render row({
|
|
833
|
+
model,
|
|
834
|
+
entry,
|
|
835
|
+
selected: selected === entry.id,
|
|
836
|
+
})}{:else}{entry.name}{/if}</span
|
|
837
|
+
>{/if}{#if entry.link}<small class="target"
|
|
838
|
+
>{entry.link.unavailable
|
|
839
|
+
? ui.labels.unavailable + " · "
|
|
840
|
+
: "→ "}{entry.link.target}</small
|
|
841
|
+
>{/if}</span
|
|
842
|
+
>
|
|
843
|
+
{:else if column === "size"}<span class="kind"
|
|
844
|
+
>{isDirectory(entry)
|
|
845
|
+
? ui.labels.folder
|
|
846
|
+
: size(entry.size)}</span
|
|
847
|
+
>
|
|
848
|
+
{:else if column === "modified"}<button
|
|
849
|
+
class="timestamp-button"
|
|
850
|
+
disabled={!ready}
|
|
851
|
+
onclick={() => (timestamp = entry.modifiedAt)}
|
|
852
|
+
><time datetime={entry.modifiedAt}
|
|
853
|
+
>{formattedDate(entry.modifiedAt, ui, ready)}</time
|
|
854
|
+
></button
|
|
855
|
+
>
|
|
856
|
+
{:else}<span class="entry-data"
|
|
857
|
+
>{publicDataValue(entry.data?.[column.dataKey])}</span
|
|
858
|
+
>{/if}{/each}
|
|
859
|
+
{#if actions || ui.actions.length}<span
|
|
860
|
+
data-dirwell-actions
|
|
861
|
+
style="grid-column:1/-1;display:flex;flex-wrap:wrap;gap:.5rem"
|
|
862
|
+
>
|
|
863
|
+
{#if actions}{@render actions({
|
|
864
|
+
model,
|
|
865
|
+
entry,
|
|
866
|
+
selected: selected === entry.id,
|
|
867
|
+
})}{/if}
|
|
868
|
+
{#each ui.actions as action}
|
|
869
|
+
{#if action === "download" && entry.href && !isDirectory(entry) && !entry.link?.unavailable}<a
|
|
870
|
+
href={safeHref(entry)}
|
|
871
|
+
download>{ui.labels.download}</a
|
|
561
872
|
>
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
873
|
+
{:else if action !== "download"}<button
|
|
874
|
+
disabled={!ready || (action === "copy-link" && !entry.href)}
|
|
875
|
+
aria-label={(action === "copy-link"
|
|
876
|
+
? ui.labels.copyLink
|
|
877
|
+
: ui.labels.copyPath) +
|
|
878
|
+
": " +
|
|
879
|
+
entry.name}
|
|
880
|
+
onclick={async () => {
|
|
881
|
+
try {
|
|
882
|
+
await copyEntryAction(action, entry);
|
|
883
|
+
error = ui.labels.copied;
|
|
884
|
+
failure = null;
|
|
885
|
+
} catch {
|
|
886
|
+
error = ui.labels.copyError;
|
|
887
|
+
failure = "copy";
|
|
888
|
+
}
|
|
889
|
+
}}
|
|
890
|
+
>{action === "copy-link"
|
|
891
|
+
? ui.labels.copyLink
|
|
892
|
+
: ui.labels.copyPath}</button
|
|
893
|
+
>{/if}
|
|
894
|
+
{/each}
|
|
895
|
+
</span>{/if}
|
|
585
896
|
</li>
|
|
586
897
|
{/each}
|
|
587
898
|
{#if end < rows.length}<li
|
|
@@ -589,42 +900,68 @@
|
|
|
589
900
|
style:height={offsets[rows.length] - offsets[end] + "px"}
|
|
590
901
|
></li>{/if}
|
|
591
902
|
</ul>
|
|
592
|
-
{#if rows.length === 0}<p>
|
|
903
|
+
{#if rows.length === 0 && !busy}<p>
|
|
904
|
+
{model.total === 0 ? ui.labels.empty : ui.labels.noMatches}
|
|
905
|
+
</p>{/if}
|
|
593
906
|
</section>
|
|
594
|
-
{#if model.page?.count > 1 && !complete}<nav
|
|
907
|
+
{#if model.page?.count > 1 && !complete}<nav
|
|
908
|
+
aria-label={ui.labels.pagination}
|
|
909
|
+
>
|
|
595
910
|
{#if model.page.previousHref}<a rel="prev" href={model.page.previousHref}
|
|
596
|
-
>
|
|
597
|
-
>{/if}
|
|
598
|
-
{
|
|
911
|
+
>{ui.labels.previous}</a
|
|
912
|
+
>{/if}
|
|
913
|
+
{label(ui, "page", { page: model.page.number, pages: model.page.count })}
|
|
914
|
+
{#if model.page.nextHref}<a rel="next" href={model.page.nextHref}
|
|
915
|
+
>{ui.labels.next}</a
|
|
599
916
|
>{/if}
|
|
600
917
|
</nav>{/if}
|
|
601
|
-
{#if footer}{@render footer({ model })}{:else}<p
|
|
602
|
-
|
|
603
|
-
|
|
918
|
+
{#if footer}{@render footer({ model })}{:else if ui.brand.footerText}<p
|
|
919
|
+
class="brand-footer"
|
|
920
|
+
>
|
|
921
|
+
{#if ui.brand.footerUrl}<a href={ui.brand.footerUrl}
|
|
922
|
+
>{ui.brand.footerText}</a
|
|
923
|
+
>{:else}{ui.brand.footerText}{/if}
|
|
924
|
+
</p>{:else}<p class="keyboard-help">
|
|
925
|
+
{ui.labels.keyboardHelp}
|
|
604
926
|
</p>{/if}
|
|
605
927
|
</main>
|
|
606
928
|
{#if timestamp}<dialog
|
|
607
929
|
bind:this={timeDialog}
|
|
608
930
|
onclose={() => (timestamp = null)}
|
|
609
931
|
>
|
|
610
|
-
<h2>
|
|
932
|
+
<h2>{ui.labels.modifiedTime}</h2>
|
|
611
933
|
<p>{timestamp}</p>
|
|
612
|
-
<p>
|
|
613
|
-
|
|
934
|
+
<p>
|
|
935
|
+
{ui.dateOptions.locale !== undefined ||
|
|
936
|
+
ui.dateOptions.timeZone !== undefined
|
|
937
|
+
? formattedDate(timestamp, ui, true)
|
|
938
|
+
: new Date(timestamp).toLocaleString()}
|
|
939
|
+
</p>
|
|
940
|
+
<button onclick={() => (timestamp = null)}>{ui.labels.close}</button>
|
|
614
941
|
</dialog>{/if}
|
|
615
942
|
{#if globalOpen}<dialog
|
|
616
943
|
bind:this={globalDialog}
|
|
617
|
-
|
|
944
|
+
aria-label={ui.labels.searchArchive}
|
|
945
|
+
onclose={closeArchive}
|
|
946
|
+
onkeydown={(event) => {
|
|
947
|
+
if (event.key === "Escape") {
|
|
948
|
+
event.preventDefault();
|
|
949
|
+
event.stopPropagation();
|
|
950
|
+
closeArchive();
|
|
951
|
+
}
|
|
952
|
+
}}
|
|
618
953
|
>
|
|
619
|
-
<h2>
|
|
954
|
+
<h2>{ui.labels.searchArchive}</h2>
|
|
620
955
|
<input
|
|
621
|
-
aria-label=
|
|
956
|
+
aria-label={ui.labels.archiveSearchInput}
|
|
622
957
|
bind:this={globalInput}
|
|
623
958
|
type="search"
|
|
624
959
|
bind:value={globalQuery}
|
|
625
|
-
oninput={
|
|
960
|
+
oninput={scheduleArchiveSearch}
|
|
626
961
|
/>
|
|
627
962
|
<p role="status">{globalStatus}</p>
|
|
963
|
+
{#if archiveFailed}<button onclick={searchArchive}>{ui.labels.retry}</button
|
|
964
|
+
>{/if}
|
|
628
965
|
<ul>
|
|
629
966
|
{#each globalRows as record}<li>
|
|
630
967
|
{#if record.href}<a
|
|
@@ -635,5 +972,5 @@
|
|
|
635
972
|
>{:else}{record.path}{/if}
|
|
636
973
|
</li>{/each}
|
|
637
974
|
</ul>
|
|
638
|
-
<button onclick={
|
|
975
|
+
<button onclick={closeArchive}>{ui.labels.close}</button>
|
|
639
976
|
</dialog>{/if}
|