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