@dirwell/theme-crosswave 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 +18 -18
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +826 -59
- package/dist/index.mjs.map +1 -1
- package/dist/model-Dr7YgDTv.d.mts +174 -0
- package/dist/model.d.mts +1 -1
- package/dist/source/Crosswave.vue +536 -57
- 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 +24 -1
- package/package.json +7 -3
- package/dist/model-D4-K54I9.d.mts +0 -53
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
watch,
|
|
10
10
|
nextTick,
|
|
11
11
|
} from "vue";
|
|
12
|
+
import { queryLiveArchive } from "./shared/live-search.mjs";
|
|
12
13
|
import { useGamepad } from "@vueuse/core";
|
|
13
14
|
import { loadListing } from "@dirwell/core/client";
|
|
14
15
|
import {
|
|
@@ -16,18 +17,28 @@ import {
|
|
|
16
17
|
applyThemeMetadata,
|
|
17
18
|
restoreThemePage,
|
|
18
19
|
} from "./shared/navigation.mjs";
|
|
20
|
+
import { readView, createViewHistory } from "./shared/view-state.mjs";
|
|
19
21
|
import { entryIcon } from "./shared/icons.mjs";
|
|
20
|
-
import { directoryLike, size
|
|
22
|
+
import { directoryLike, size } from "./shared/records.mjs";
|
|
23
|
+
import {
|
|
24
|
+
resolveCustomization,
|
|
25
|
+
label,
|
|
26
|
+
formattedDate,
|
|
27
|
+
copyEntryAction,
|
|
28
|
+
publicDataValue,
|
|
29
|
+
} from "./shared/customization.mjs";
|
|
21
30
|
import { createWave } from "./wave.mjs";
|
|
22
31
|
const props = defineProps({ model: { type: Object, required: true } }),
|
|
23
32
|
model = shallowRef(restoreThemePage(props.model, Boolean(import.meta.hot))),
|
|
24
|
-
entries = shallowRef(model.value.entries)
|
|
33
|
+
entries = shallowRef(model.value.entries),
|
|
34
|
+
ui = computed(() => resolveCustomization(model.value.ui));
|
|
25
35
|
const query = ref(""),
|
|
26
|
-
category = ref(
|
|
36
|
+
category = ref(model.value.categories[0]?.id ?? null),
|
|
27
37
|
selected = ref(null),
|
|
28
38
|
ready = ref(false),
|
|
29
39
|
busy = ref(false),
|
|
30
40
|
error = ref(""),
|
|
41
|
+
failure = ref(null),
|
|
31
42
|
color = ref(props.model.color),
|
|
32
43
|
paused = ref(!props.model.backgroundMotion),
|
|
33
44
|
clock = ref(""),
|
|
@@ -35,14 +46,107 @@ const query = ref(""),
|
|
|
35
46
|
waveReady = ref(false),
|
|
36
47
|
pendingUpdates = ref(null),
|
|
37
48
|
canvas = ref(),
|
|
38
|
-
search = ref()
|
|
49
|
+
search = ref(),
|
|
50
|
+
listScroll = ref(),
|
|
51
|
+
footerElement = ref(),
|
|
52
|
+
scrollTop = ref(0),
|
|
53
|
+
viewportHeight = ref(400),
|
|
54
|
+
retryRequest = shallowRef(null),
|
|
55
|
+
heights = shallowRef(new Map());
|
|
56
|
+
const archiveOpen = ref(false),
|
|
57
|
+
archiveQuery = ref(""),
|
|
58
|
+
archiveRows = shallowRef([]),
|
|
59
|
+
archiveStatus = ref(""),
|
|
60
|
+
archiveFailed = ref(false),
|
|
61
|
+
archiveTrigger = ref(),
|
|
62
|
+
archiveDialog = ref(),
|
|
63
|
+
archiveInput = ref();
|
|
64
|
+
let archiveAbort,
|
|
65
|
+
archiveRequest = 0,
|
|
66
|
+
archivePending = false,
|
|
67
|
+
archiveDebouncing = false;
|
|
68
|
+
async function searchArchive() {
|
|
69
|
+
const token = ++archiveRequest;
|
|
70
|
+
archiveAbort?.abort();
|
|
71
|
+
const local = (archiveAbort = new AbortController());
|
|
72
|
+
archiveFailed.value = false;
|
|
73
|
+
if (!archiveQuery.value.trim()) {
|
|
74
|
+
archiveRows.value = [];
|
|
75
|
+
archiveStatus.value = ui.value.labels.beginArchive;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
archiveStatus.value = ui.value.labels.searchingArchive;
|
|
79
|
+
archivePending = true;
|
|
80
|
+
try {
|
|
81
|
+
const result = await queryLiveArchive(
|
|
82
|
+
model.value.searchQueryHref,
|
|
83
|
+
archiveQuery.value.trim(),
|
|
84
|
+
local.signal,
|
|
85
|
+
);
|
|
86
|
+
if (local.signal.aborted || token !== archiveRequest) return;
|
|
87
|
+
archiveRows.value = result.records;
|
|
88
|
+
archiveStatus.value = result.truncated
|
|
89
|
+
? label(ui.value, "truncatedResults", {
|
|
90
|
+
shown: result.records.length,
|
|
91
|
+
count: result.records.length + "+",
|
|
92
|
+
})
|
|
93
|
+
: label(ui.value, "archiveResults", { count: result.records.length });
|
|
94
|
+
} catch {
|
|
95
|
+
if (local.signal.aborted || token !== archiveRequest) return;
|
|
96
|
+
archiveRows.value = [];
|
|
97
|
+
archiveStatus.value = ui.value.labels.archiveError;
|
|
98
|
+
archiveFailed.value = true;
|
|
99
|
+
} finally {
|
|
100
|
+
if (token === archiveRequest) archivePending = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
watch([archiveOpen, archiveQuery], (_, __, cleanup) => {
|
|
104
|
+
archiveAbort?.abort();
|
|
105
|
+
archiveRequest++;
|
|
106
|
+
archivePending = false;
|
|
107
|
+
if (!archiveOpen.value) return;
|
|
108
|
+
archiveDebouncing = true;
|
|
109
|
+
const debounce = setTimeout(() => {
|
|
110
|
+
archiveDebouncing = false;
|
|
111
|
+
searchArchive();
|
|
112
|
+
}, 300);
|
|
113
|
+
cleanup(() => {
|
|
114
|
+
clearTimeout(debounce);
|
|
115
|
+
archiveDebouncing = false;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
watch(archiveOpen, async (open, _, cleanup) => {
|
|
119
|
+
if (!open) {
|
|
120
|
+
await nextTick();
|
|
121
|
+
archiveTrigger.value?.focus();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await nextTick();
|
|
125
|
+
if (!archiveOpen.value) return;
|
|
126
|
+
archiveDialog.value?.showModal();
|
|
127
|
+
archiveInput.value?.focus();
|
|
128
|
+
const timer = setInterval(() => {
|
|
129
|
+
if (
|
|
130
|
+
document.visibilityState === "visible" &&
|
|
131
|
+
archiveQuery.value.trim() &&
|
|
132
|
+
!archivePending &&
|
|
133
|
+
!archiveDebouncing
|
|
134
|
+
)
|
|
135
|
+
searchArchive();
|
|
136
|
+
}, model.value.searchRefreshIntervalMs ?? 2000);
|
|
137
|
+
cleanup(() => {
|
|
138
|
+
clearInterval(timer);
|
|
139
|
+
archiveAbort?.abort();
|
|
140
|
+
archiveRequest++;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
39
143
|
const complete = computed(() => entries.value.length === model.value.total),
|
|
40
144
|
membership = (entry) =>
|
|
41
145
|
Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
|
|
42
146
|
const rows = computed(() =>
|
|
43
147
|
entries.value.filter(
|
|
44
148
|
(entry) =>
|
|
45
|
-
|
|
149
|
+
membership(entry).includes(category.value) &&
|
|
46
150
|
`${entry.name} ${entry.link?.target ?? ""}`
|
|
47
151
|
.toLocaleLowerCase()
|
|
48
152
|
.includes(query.value.trim().toLocaleLowerCase()),
|
|
@@ -50,9 +154,116 @@ const rows = computed(() =>
|
|
|
50
154
|
),
|
|
51
155
|
active = computed(() => rows.value.find((entry) => entry.id === selected.value) ?? rows.value[0]),
|
|
52
156
|
heading = computed(
|
|
53
|
-
() =>
|
|
157
|
+
() =>
|
|
158
|
+
model.value.categories.find((item) => item.id === category.value)?.label ??
|
|
159
|
+
ui.value.labels.allFiles,
|
|
54
160
|
);
|
|
55
161
|
const nodes = new Map();
|
|
162
|
+
const virtual = computed(() => ready.value && rows.value.length > 500);
|
|
163
|
+
const offsets = computed(() => {
|
|
164
|
+
const result = [0];
|
|
165
|
+
for (const entry of rows.value) result.push(result.at(-1) + (heights.value.get(entry.id) ?? 76));
|
|
166
|
+
return result;
|
|
167
|
+
});
|
|
168
|
+
const start = computed(() => {
|
|
169
|
+
if (!virtual.value) return 0;
|
|
170
|
+
const position = Math.max(0, scrollTop.value - (model.value.parentHref ? 76 : 0));
|
|
171
|
+
let low = 0,
|
|
172
|
+
high = rows.value.length;
|
|
173
|
+
while (low < high) {
|
|
174
|
+
const middle = (low + high) >> 1;
|
|
175
|
+
if (offsets.value[middle + 1] <= position) low = middle + 1;
|
|
176
|
+
else high = middle;
|
|
177
|
+
}
|
|
178
|
+
return Math.max(0, low - 5);
|
|
179
|
+
});
|
|
180
|
+
const end = computed(() => {
|
|
181
|
+
if (!virtual.value) return rows.value.length;
|
|
182
|
+
let low = 0,
|
|
183
|
+
high = rows.value.length;
|
|
184
|
+
while (low < high) {
|
|
185
|
+
const middle = (low + high) >> 1;
|
|
186
|
+
if (offsets.value[middle + 1] <= scrollTop.value + viewportHeight.value) low = middle + 1;
|
|
187
|
+
else high = middle;
|
|
188
|
+
}
|
|
189
|
+
return Math.min(rows.value.length, Math.max(start.value + 35, low + 5));
|
|
190
|
+
});
|
|
191
|
+
const visibleRows = computed(() => rows.value.slice(start.value, end.value));
|
|
192
|
+
const heightCache = new Map();
|
|
193
|
+
let rowObserver, layoutObserver, viewHistory, measuredWidth;
|
|
194
|
+
function measureLayout() {
|
|
195
|
+
if (!listScroll.value) return;
|
|
196
|
+
const width = listScroll.value.clientWidth;
|
|
197
|
+
if (measuredWidth !== undefined && width !== measuredWidth) {
|
|
198
|
+
heightCache.clear();
|
|
199
|
+
heights.value = new Map(
|
|
200
|
+
[...nodes].map(([id, node]) => [id, node.getBoundingClientRect().height]),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
measuredWidth = width;
|
|
204
|
+
const footerHeight = footerElement.value?.getBoundingClientRect().height ?? 82;
|
|
205
|
+
document.documentElement.style.setProperty("--cw-footer-height", footerHeight + "px");
|
|
206
|
+
const available = Math.max(
|
|
207
|
+
160,
|
|
208
|
+
innerHeight - listScroll.value.getBoundingClientRect().top - footerHeight - 24,
|
|
209
|
+
);
|
|
210
|
+
viewportHeight.value = available;
|
|
211
|
+
listScroll.value.style.setProperty("--cw-list-height", available + "px");
|
|
212
|
+
}
|
|
213
|
+
watch([virtual, () => model.value.path], () => nextTick(measureLayout));
|
|
214
|
+
const viewDefaults = (page) => ({ query: "", category: page.categories[0]?.id ?? null });
|
|
215
|
+
function saveView() {
|
|
216
|
+
if (!viewHistory) return;
|
|
217
|
+
viewHistory.save(
|
|
218
|
+
new URL(model.value.dataHref, document.baseURI).href,
|
|
219
|
+
model.value.path,
|
|
220
|
+
{
|
|
221
|
+
query: query.value,
|
|
222
|
+
category: category.value,
|
|
223
|
+
selected: selected.value,
|
|
224
|
+
windowY: window.scrollY,
|
|
225
|
+
listTop: listScroll.value?.scrollTop ?? 0,
|
|
226
|
+
},
|
|
227
|
+
viewDefaults(model.value),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function retry() {
|
|
231
|
+
if (failure.value === "folder" && retryRequest.value) openFolder(...retryRequest.value, true);
|
|
232
|
+
else openFolder(model.value.dataHref, location.href, false, true);
|
|
233
|
+
}
|
|
234
|
+
watchEffect((onCleanup) => {
|
|
235
|
+
if (!ready.value || busy.value) return;
|
|
236
|
+
const view = {
|
|
237
|
+
query: query.value,
|
|
238
|
+
category: category.value,
|
|
239
|
+
selected: selected.value,
|
|
240
|
+
listTop: scrollTop.value,
|
|
241
|
+
};
|
|
242
|
+
const dataHref = model.value.dataHref,
|
|
243
|
+
path = model.value.path,
|
|
244
|
+
defaults = viewDefaults(model.value);
|
|
245
|
+
const frame = requestAnimationFrame(() =>
|
|
246
|
+
viewHistory?.save(
|
|
247
|
+
new URL(dataHref, document.baseURI).href,
|
|
248
|
+
path,
|
|
249
|
+
{ ...view, windowY: window.scrollY },
|
|
250
|
+
defaults,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
onCleanup(() => cancelAnimationFrame(frame));
|
|
254
|
+
});
|
|
255
|
+
function setNode(id, node) {
|
|
256
|
+
const old = nodes.get(id);
|
|
257
|
+
if (old && old !== node) rowObserver?.unobserve(old);
|
|
258
|
+
if (node) {
|
|
259
|
+
nodes.set(id, node);
|
|
260
|
+
rowObserver?.observe(node);
|
|
261
|
+
} else nodes.delete(id);
|
|
262
|
+
}
|
|
263
|
+
watch([query, category], () => {
|
|
264
|
+
scrollTop.value = 0;
|
|
265
|
+
if (listScroll.value) listScroll.value.scrollTop = 0;
|
|
266
|
+
});
|
|
56
267
|
let wave,
|
|
57
268
|
gamepad,
|
|
58
269
|
abort,
|
|
@@ -84,45 +295,95 @@ async function loadComplete() {
|
|
|
84
295
|
});
|
|
85
296
|
if (token === navigation && !local.signal.aborted && !disposed) entries.value = data.entries;
|
|
86
297
|
} catch (cause) {
|
|
87
|
-
if (token === navigation && !local.signal.aborted)
|
|
88
|
-
error.value =
|
|
298
|
+
if (token === navigation && !local.signal.aborted) {
|
|
299
|
+
error.value = ui.value.labels.listingError;
|
|
300
|
+
failure.value = "listing";
|
|
301
|
+
}
|
|
89
302
|
} finally {
|
|
90
303
|
if (token === navigation) busy.value = false;
|
|
91
304
|
}
|
|
92
305
|
}
|
|
93
|
-
async function openFolder(
|
|
306
|
+
async function openFolder(
|
|
307
|
+
dataHref,
|
|
308
|
+
url,
|
|
309
|
+
push,
|
|
310
|
+
refresh = false,
|
|
311
|
+
restoreState = null,
|
|
312
|
+
fresh = false,
|
|
313
|
+
) {
|
|
314
|
+
if (push) saveView();
|
|
94
315
|
const token = ++navigation;
|
|
95
316
|
abort?.abort();
|
|
96
317
|
const local = (abort = new AbortController());
|
|
97
318
|
busy.value = true;
|
|
98
319
|
error.value = "";
|
|
320
|
+
failure.value = null;
|
|
99
321
|
try {
|
|
100
|
-
const next = await fetchThemePage(dataHref, url, "crosswave", local.signal, refresh);
|
|
322
|
+
const next = await fetchThemePage(dataHref, url, "crosswave", local.signal, refresh || fresh);
|
|
101
323
|
let nextEntries = next.entries;
|
|
102
324
|
if (refresh && next.manifestHref && nextEntries.length !== next.total)
|
|
103
325
|
nextEntries = (
|
|
104
326
|
await loadListing(next.manifestHref, { signal: local.signal, cache: "no-store" })
|
|
105
327
|
).entries;
|
|
106
328
|
if (token !== navigation || local.signal.aborted || disposed) return;
|
|
329
|
+
if (!refresh) viewHistory.commit(next.dataHref, next.path, url, push);
|
|
330
|
+
if (next.path !== model.value.path) {
|
|
331
|
+
heightCache.set(model.value.path, heights.value);
|
|
332
|
+
if (heightCache.size > 8) heightCache.delete(heightCache.keys().next().value);
|
|
333
|
+
heights.value = heightCache.get(next.path) ?? new Map();
|
|
334
|
+
}
|
|
107
335
|
model.value = next;
|
|
336
|
+
retryRequest.value = null;
|
|
108
337
|
entries.value = nextEntries;
|
|
109
338
|
if (!refresh) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
339
|
+
const view = readView(
|
|
340
|
+
url,
|
|
341
|
+
"crosswave",
|
|
342
|
+
next.path,
|
|
343
|
+
viewDefaults(next),
|
|
344
|
+
next.categories.map((item) => item.id),
|
|
345
|
+
restoreState,
|
|
346
|
+
);
|
|
347
|
+
query.value = view.query;
|
|
348
|
+
selected.value = view.selected;
|
|
349
|
+
category.value = view.category;
|
|
113
350
|
}
|
|
114
|
-
if (push) history.pushState({ dataHref: next.dataHref }, "", url);
|
|
115
351
|
applyThemeMetadata(next, url);
|
|
116
352
|
await loadComplete();
|
|
353
|
+
if (!refresh) {
|
|
354
|
+
const view = readView(
|
|
355
|
+
url,
|
|
356
|
+
"crosswave",
|
|
357
|
+
next.path,
|
|
358
|
+
viewDefaults(next),
|
|
359
|
+
next.categories.map((item) => item.id),
|
|
360
|
+
restoreState,
|
|
361
|
+
);
|
|
362
|
+
await nextTick();
|
|
363
|
+
if (selected.value && !rows.value.some((entry) => entry.id === selected.value))
|
|
364
|
+
selected.value = null;
|
|
365
|
+
scrollTop.value = view.listTop;
|
|
366
|
+
if (listScroll.value) listScroll.value.scrollTop = view.listTop;
|
|
367
|
+
await nextTick();
|
|
368
|
+
window.scrollTo({ top: view.windowY });
|
|
369
|
+
if (restoreState && selected.value)
|
|
370
|
+
nodes.get(selected.value)?.querySelector("a")?.focus({ preventScroll: true });
|
|
371
|
+
}
|
|
117
372
|
} catch (cause) {
|
|
118
|
-
if (token === navigation && !local.signal.aborted)
|
|
119
|
-
error.value =
|
|
373
|
+
if (token === navigation && !local.signal.aborted) {
|
|
374
|
+
error.value = ui.value.labels.folderError;
|
|
375
|
+
failure.value = "folder";
|
|
376
|
+
retryRequest.value = [dataHref, url, push, refresh, restoreState];
|
|
377
|
+
viewHistory?.rollback();
|
|
378
|
+
}
|
|
120
379
|
} finally {
|
|
121
380
|
if (token === navigation) busy.value = false;
|
|
122
381
|
}
|
|
123
382
|
}
|
|
124
383
|
function navigateLink(event, href, dataHref) {
|
|
125
384
|
if (
|
|
385
|
+
typeof href !== "string" ||
|
|
386
|
+
typeof dataHref !== "string" ||
|
|
126
387
|
event?.altKey ||
|
|
127
388
|
event?.ctrlKey ||
|
|
128
389
|
event?.metaKey ||
|
|
@@ -135,6 +396,7 @@ function navigateLink(event, href, dataHref) {
|
|
|
135
396
|
openFolder(dataHref, new URL(href, document.baseURI).href, true);
|
|
136
397
|
}
|
|
137
398
|
async function openEntry(event, entry) {
|
|
399
|
+
saveView();
|
|
138
400
|
const href = entry.presentation?.folderDataHref;
|
|
139
401
|
if (
|
|
140
402
|
entry.exitsExplorer ||
|
|
@@ -151,28 +413,41 @@ async function openEntry(event, entry) {
|
|
|
151
413
|
await openFolder(href, new URL(entry.href, document.baseURI).href, true);
|
|
152
414
|
}
|
|
153
415
|
async function move(index) {
|
|
154
|
-
const
|
|
416
|
+
const target = Math.max(0, Math.min(rows.value.length - 1, index));
|
|
417
|
+
const entry = rows.value[target];
|
|
155
418
|
if (!entry) return;
|
|
156
419
|
selected.value = entry.id;
|
|
420
|
+
if (virtual.value) {
|
|
421
|
+
scrollTop.value = Math.max(0, offsets.value[target] - 100 + (model.value.parentHref ? 76 : 0));
|
|
422
|
+
if (listScroll.value) listScroll.value.scrollTop = scrollTop.value;
|
|
423
|
+
}
|
|
157
424
|
await nextTick();
|
|
158
425
|
const node = nodes.get(entry.id);
|
|
159
426
|
node?.querySelector("a")?.focus({ preventScroll: true });
|
|
160
427
|
node?.scrollIntoView({ block: "nearest" });
|
|
161
428
|
}
|
|
162
|
-
function stepCategory(delta) {
|
|
429
|
+
async function stepCategory(delta) {
|
|
430
|
+
if (ui.value.direction === "rtl") delta = -delta;
|
|
431
|
+
const moveFocus = document.activeElement?.getAttribute("role") === "tab";
|
|
163
432
|
const items = model.value.categories,
|
|
164
433
|
index = items.findIndex((item) => item.id === category.value);
|
|
165
434
|
category.value = items[(index + delta + items.length) % items.length].id;
|
|
166
435
|
selected.value = null;
|
|
436
|
+
if (moveFocus) {
|
|
437
|
+
await nextTick();
|
|
438
|
+
document.getElementById("cw-tab-" + category.value)?.focus({ preventScroll: true });
|
|
439
|
+
}
|
|
167
440
|
}
|
|
168
|
-
function confirm() {
|
|
441
|
+
async function confirm() {
|
|
169
442
|
if (!active.value || busy.value) return;
|
|
443
|
+
await move(rows.value.findIndex((entry) => entry.id === active.value.id));
|
|
170
444
|
const anchor = nodes.get(active.value.id)?.querySelector("a");
|
|
171
445
|
anchor?.click();
|
|
172
446
|
}
|
|
173
447
|
function parent() {
|
|
174
448
|
if (model.value.parentHref) {
|
|
175
|
-
if (location.protocol === "file:"
|
|
449
|
+
if (location.protocol === "file:" || typeof model.value.parentDataHref !== "string")
|
|
450
|
+
location.href = model.value.parentHref;
|
|
176
451
|
else
|
|
177
452
|
openFolder(
|
|
178
453
|
model.value.parentDataHref,
|
|
@@ -182,7 +457,9 @@ function parent() {
|
|
|
182
457
|
}
|
|
183
458
|
}
|
|
184
459
|
function keyboard(event) {
|
|
460
|
+
if (archiveOpen.value) return;
|
|
185
461
|
if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
462
|
+
if (event.target?.closest?.("[data-dirwell-actions]")) return;
|
|
186
463
|
const editing = /INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "");
|
|
187
464
|
if (editing) {
|
|
188
465
|
if (event.key === "Escape") query.value = "";
|
|
@@ -213,6 +490,7 @@ function keyboard(event) {
|
|
|
213
490
|
}
|
|
214
491
|
}
|
|
215
492
|
function controller(value) {
|
|
493
|
+
if (archiveOpen.value) return;
|
|
216
494
|
pad.value = !!value;
|
|
217
495
|
if (!value) {
|
|
218
496
|
pressed.clear();
|
|
@@ -276,7 +554,7 @@ const colors = {
|
|
|
276
554
|
};
|
|
277
555
|
const onPop = (event) =>
|
|
278
556
|
event.state?.dataHref
|
|
279
|
-
? openFolder(event.state.dataHref, location.href, false)
|
|
557
|
+
? openFolder(event.state.dataHref, location.href, false, false, event.state)
|
|
280
558
|
: location.reload();
|
|
281
559
|
const suspend = () => {
|
|
282
560
|
gamepad?.pause();
|
|
@@ -298,7 +576,43 @@ onMounted(() => {
|
|
|
298
576
|
ready.value = true;
|
|
299
577
|
document.body.setAttribute("data-cw-enhanced", "");
|
|
300
578
|
abort = new AbortController();
|
|
301
|
-
|
|
579
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
580
|
+
rowObserver = new ResizeObserver((observations) => {
|
|
581
|
+
const next = new Map(heights.value);
|
|
582
|
+
let changed = false;
|
|
583
|
+
for (const observation of observations) {
|
|
584
|
+
const id = observation.target.dataset.id;
|
|
585
|
+
const height = observation.target.getBoundingClientRect().height;
|
|
586
|
+
if (height > 0 && next.get(id) !== height) {
|
|
587
|
+
next.set(id, height);
|
|
588
|
+
changed = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (changed) heights.value = next;
|
|
592
|
+
});
|
|
593
|
+
for (const node of nodes.values()) rowObserver.observe(node);
|
|
594
|
+
layoutObserver = new ResizeObserver(measureLayout);
|
|
595
|
+
if (footerElement.value) layoutObserver.observe(footerElement.value);
|
|
596
|
+
const header = document.querySelector("[data-dirwell-theme='crosswave'] .cw-top");
|
|
597
|
+
if (header) layoutObserver.observe(header);
|
|
598
|
+
}
|
|
599
|
+
measureLayout();
|
|
600
|
+
window.addEventListener("resize", measureLayout);
|
|
601
|
+
viewHistory = createViewHistory("crosswave");
|
|
602
|
+
const view = readView(
|
|
603
|
+
location.href,
|
|
604
|
+
"crosswave",
|
|
605
|
+
model.value.path,
|
|
606
|
+
viewDefaults(model.value),
|
|
607
|
+
model.value.categories.map((item) => item.id),
|
|
608
|
+
history.state,
|
|
609
|
+
);
|
|
610
|
+
query.value = view.query;
|
|
611
|
+
category.value = view.category;
|
|
612
|
+
selected.value = view.selected;
|
|
613
|
+
scrollTop.value = view.listTop;
|
|
614
|
+
if (listScroll.value) listScroll.value.scrollTop = view.listTop;
|
|
615
|
+
saveView();
|
|
302
616
|
try {
|
|
303
617
|
const stored = localStorage.getItem("dirwell-crosswave-color");
|
|
304
618
|
if (Object.hasOwn(colors, stored)) color.value = stored;
|
|
@@ -347,7 +661,11 @@ onMounted(() => {
|
|
|
347
661
|
);
|
|
348
662
|
}
|
|
349
663
|
const updateClock = () =>
|
|
350
|
-
(clock.value = new Date().toLocaleTimeString(
|
|
664
|
+
(clock.value = new Date().toLocaleTimeString(ui.value.dateOptions.locale, {
|
|
665
|
+
timeZone: ui.value.dateOptions.timeZone,
|
|
666
|
+
hour: "2-digit",
|
|
667
|
+
minute: "2-digit",
|
|
668
|
+
}));
|
|
351
669
|
updateClock();
|
|
352
670
|
timer = setInterval(updateClock, 1000);
|
|
353
671
|
window.addEventListener("keydown", keyboard);
|
|
@@ -368,9 +686,15 @@ watch(
|
|
|
368
686
|
if (!href || typeof EventSource === "undefined") return;
|
|
369
687
|
const updates = new EventSource(href);
|
|
370
688
|
updates.onmessage = () => (pendingUpdates.value = href);
|
|
371
|
-
updates.onerror = () =>
|
|
689
|
+
updates.onerror = () => {
|
|
690
|
+
error.value = ui.value.labels.updatesError;
|
|
691
|
+
failure.value = "updates";
|
|
692
|
+
};
|
|
372
693
|
updates.onopen = () => {
|
|
373
|
-
if (
|
|
694
|
+
if (failure.value === "updates") {
|
|
695
|
+
error.value = "";
|
|
696
|
+
failure.value = null;
|
|
697
|
+
}
|
|
374
698
|
};
|
|
375
699
|
onCleanup(() => updates.close());
|
|
376
700
|
},
|
|
@@ -394,9 +718,13 @@ watchEffect(() => {
|
|
|
394
718
|
}
|
|
395
719
|
});
|
|
396
720
|
onBeforeUnmount(() => {
|
|
721
|
+
archiveAbort?.abort();
|
|
397
722
|
disposed = true;
|
|
398
723
|
document.body.removeAttribute("data-cw-enhanced");
|
|
399
724
|
abort?.abort();
|
|
725
|
+
rowObserver?.disconnect();
|
|
726
|
+
layoutObserver?.disconnect();
|
|
727
|
+
window.removeEventListener("resize", measureLayout);
|
|
400
728
|
gamepad?.pause();
|
|
401
729
|
wave?.dispose();
|
|
402
730
|
clearInterval(timer);
|
|
@@ -418,16 +746,24 @@ onBeforeUnmount(() => {
|
|
|
418
746
|
<header class="cw-top">
|
|
419
747
|
<div class="cw-brand">
|
|
420
748
|
<slot name="header" :model="model">
|
|
749
|
+
<img
|
|
750
|
+
v-if="ui.brand.logoUrl"
|
|
751
|
+
:src="ui.brand.logoUrl"
|
|
752
|
+
:alt="ui.brand.logoAlt ?? ''"
|
|
753
|
+
width="40"
|
|
754
|
+
height="40"
|
|
755
|
+
/>
|
|
421
756
|
<a
|
|
422
757
|
:href="model.rootHref"
|
|
423
758
|
@click="navigateLink($event, model.rootHref, model.rootDataHref)"
|
|
424
|
-
>{{ model.title }}</a
|
|
759
|
+
>{{ ui.brand.name ?? model.title }}</a
|
|
425
760
|
></slot
|
|
426
761
|
>
|
|
427
762
|
</div>
|
|
428
|
-
<nav class="cw-location" aria-label="
|
|
429
|
-
<a :href="model.rootHref" @click="navigateLink($event, model.rootHref, model.rootDataHref)"
|
|
430
|
-
|
|
763
|
+
<nav class="cw-location" :aria-label="ui.labels.breadcrumb">
|
|
764
|
+
<a :href="model.rootHref" @click="navigateLink($event, model.rootHref, model.rootDataHref)">{{
|
|
765
|
+
ui.labels.home
|
|
766
|
+
}}</a
|
|
431
767
|
><template v-for="crumb in model.breadcrumbs" :key="crumb.href">
|
|
432
768
|
/
|
|
433
769
|
<a :href="crumb.href" @click="navigateLink($event, crumb.href, crumb.dataHref)">{{
|
|
@@ -438,8 +774,8 @@ onBeforeUnmount(() => {
|
|
|
438
774
|
<div class="cw-clock" v-if="ready">{{ clock }}</div>
|
|
439
775
|
</header>
|
|
440
776
|
<main class="cw-stage" data-framework="vue" :aria-busy="busy">
|
|
441
|
-
<h1 class="cw-sr">
|
|
442
|
-
<nav class="cw-rail" aria-label="
|
|
777
|
+
<h1 class="cw-sr">{{ label(ui, "filesIn", { path: model.path || "/" }) }}</h1>
|
|
778
|
+
<nav class="cw-rail" :aria-label="ui.labels.categories" :hidden="!ready">
|
|
443
779
|
<div class="cw-categories" role="tablist">
|
|
444
780
|
<button
|
|
445
781
|
v-for="item in model.categories"
|
|
@@ -472,13 +808,30 @@ onBeforeUnmount(() => {
|
|
|
472
808
|
<div class="cw-list-head">
|
|
473
809
|
<div class="cw-directory">
|
|
474
810
|
<h2>{{ heading }}</h2>
|
|
475
|
-
<span data-cw-count>{{
|
|
811
|
+
<span data-cw-count>{{
|
|
812
|
+
label(ui, "items", { count: complete ? rows.length : entries.length })
|
|
813
|
+
}}</span>
|
|
476
814
|
</div>
|
|
477
815
|
<label class="cw-search"
|
|
478
|
-
>
|
|
816
|
+
><span>{{ ui.labels.search }}</span
|
|
817
|
+
><input ref="search" type="search" v-model="query" :disabled="!ready || !complete"
|
|
479
818
|
/></label>
|
|
819
|
+
<button
|
|
820
|
+
class="cw-archive-trigger"
|
|
821
|
+
ref="archiveTrigger"
|
|
822
|
+
v-if="model.searchQueryHref"
|
|
823
|
+
:disabled="!ready"
|
|
824
|
+
@click="archiveOpen = true"
|
|
825
|
+
>
|
|
826
|
+
{{ ui.labels.searchArchive }}
|
|
827
|
+
</button>
|
|
480
828
|
</div>
|
|
481
|
-
<div
|
|
829
|
+
<div
|
|
830
|
+
class="cw-list-scroll"
|
|
831
|
+
:data-virtual="virtual"
|
|
832
|
+
ref="listScroll"
|
|
833
|
+
@scroll="scrollTop = $event.currentTarget.scrollTop"
|
|
834
|
+
>
|
|
482
835
|
<ul class="cw-files">
|
|
483
836
|
<li v-if="model.parentHref" class="cw-row">
|
|
484
837
|
<a
|
|
@@ -488,12 +841,16 @@ onBeforeUnmount(() => {
|
|
|
488
841
|
>../</a
|
|
489
842
|
>
|
|
490
843
|
</li>
|
|
844
|
+
<li v-if="start" aria-hidden="true" :style="{ height: offsets[start] + 'px' }"></li>
|
|
491
845
|
<li
|
|
492
|
-
v-for="entry in
|
|
846
|
+
v-for="(entry, visibleIndex) in visibleRows"
|
|
493
847
|
:key="entry.id"
|
|
494
|
-
:ref="(node) => (
|
|
848
|
+
:ref="(node) => setNode(entry.id, node)"
|
|
849
|
+
:aria-posinset="start + visibleIndex + 1"
|
|
850
|
+
:aria-setsize="rows.length"
|
|
495
851
|
:class="['cw-row', { 'cw-selected': active?.id === entry.id }]"
|
|
496
852
|
data-cw-entry
|
|
853
|
+
:data-custom-columns="Boolean(model.ui?.columns)"
|
|
497
854
|
:data-id="entry.id"
|
|
498
855
|
>
|
|
499
856
|
<a
|
|
@@ -519,13 +876,30 @@ onBeforeUnmount(() => {
|
|
|
519
876
|
"
|
|
520
877
|
></span>
|
|
521
878
|
<span class="cw-entry-label">
|
|
522
|
-
<
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
879
|
+
<template
|
|
880
|
+
v-for="column in ui.columns"
|
|
881
|
+
:key="typeof column === 'string' ? column : column.dataKey"
|
|
882
|
+
><span v-if="column === 'name'" class="cw-name"
|
|
883
|
+
>{{ entry.name }}{{ directoryLike(entry) ? "/" : "" }}</span
|
|
884
|
+
>
|
|
885
|
+
<span v-if="entry.link && column === 'name'" class="cw-target">{{
|
|
886
|
+
entry.link.target
|
|
887
|
+
}}</span>
|
|
888
|
+
<span v-if="column === 'size'" class="cw-row-meta"
|
|
889
|
+
>{{
|
|
890
|
+
entry.link?.unavailable
|
|
891
|
+
? ui.labels.unavailable
|
|
892
|
+
: ui.labels[entry.kind === "symlink" ? "link" : entry.kind]
|
|
893
|
+
}}
|
|
894
|
+
· {{ directoryLike(entry) ? ui.labels.folder : size(entry.size) }}</span
|
|
895
|
+
>
|
|
896
|
+
<span v-if="column === 'modified'" class="cw-row-date"
|
|
897
|
+
>{{ ui.labels.modified }}
|
|
898
|
+
{{ formattedDate(entry.modifiedAt, ui, ready) }}</span
|
|
899
|
+
><span v-if="typeof column === 'object'" class="cw-data-value"
|
|
900
|
+
>{{ column.label }}: {{ publicDataValue(entry.data?.[column.dataKey]) }}</span
|
|
901
|
+
></template
|
|
527
902
|
>
|
|
528
|
-
<span class="cw-row-date">Modified {{ modified(entry.modifiedAt) }}</span>
|
|
529
903
|
</span></slot
|
|
530
904
|
></a
|
|
531
905
|
><span v-else class="cw-entry cw-unavailable"
|
|
@@ -533,37 +907,142 @@ onBeforeUnmount(() => {
|
|
|
533
907
|
entry.name
|
|
534
908
|
}}</slot></span
|
|
535
909
|
>
|
|
910
|
+
<span
|
|
911
|
+
v-if="ui.actions.length || $slots.actions"
|
|
912
|
+
data-dirwell-actions
|
|
913
|
+
class="cw-row-actions"
|
|
914
|
+
>
|
|
915
|
+
<slot
|
|
916
|
+
name="actions"
|
|
917
|
+
:model="model"
|
|
918
|
+
:entry="entry"
|
|
919
|
+
:selected="selected === entry.id"
|
|
920
|
+
/>
|
|
921
|
+
<template v-for="action in ui.actions" :key="action">
|
|
922
|
+
<a
|
|
923
|
+
v-if="
|
|
924
|
+
action === 'download' &&
|
|
925
|
+
entry.href &&
|
|
926
|
+
!directoryLike(entry) &&
|
|
927
|
+
!entry.link?.unavailable
|
|
928
|
+
"
|
|
929
|
+
:href="safeHref(entry)"
|
|
930
|
+
download
|
|
931
|
+
>{{ ui.labels.download }}</a
|
|
932
|
+
>
|
|
933
|
+
<button
|
|
934
|
+
v-else-if="action !== 'download'"
|
|
935
|
+
:disabled="!ready || (action === 'copy-link' && !entry.href)"
|
|
936
|
+
:aria-label="
|
|
937
|
+
(action === 'copy-link' ? ui.labels.copyLink : ui.labels.copyPath) +
|
|
938
|
+
': ' +
|
|
939
|
+
entry.name
|
|
940
|
+
"
|
|
941
|
+
@click="
|
|
942
|
+
async () => {
|
|
943
|
+
try {
|
|
944
|
+
await copyEntryAction(action, entry);
|
|
945
|
+
error = ui.labels.copied;
|
|
946
|
+
failure = null;
|
|
947
|
+
} catch {
|
|
948
|
+
error = ui.labels.copyError;
|
|
949
|
+
failure = 'copy';
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
"
|
|
953
|
+
>
|
|
954
|
+
{{ action === "copy-link" ? ui.labels.copyLink : ui.labels.copyPath }}
|
|
955
|
+
</button>
|
|
956
|
+
</template>
|
|
957
|
+
</span>
|
|
536
958
|
</li>
|
|
959
|
+
<li
|
|
960
|
+
v-if="end < rows.length"
|
|
961
|
+
aria-hidden="true"
|
|
962
|
+
:style="{ height: offsets[rows.length] - offsets[end] + 'px' }"
|
|
963
|
+
></li>
|
|
537
964
|
</ul>
|
|
538
|
-
<p class="cw-empty" v-if="!rows.length
|
|
965
|
+
<p class="cw-empty" v-if="!rows.length && !busy">
|
|
966
|
+
{{ model.total === 0 ? ui.labels.empty : ui.labels.noMatches }}
|
|
967
|
+
</p>
|
|
539
968
|
</div>
|
|
540
969
|
</section>
|
|
541
970
|
<aside class="cw-detail" v-if="ready && active">
|
|
542
971
|
<h2>{{ active.name }}</h2>
|
|
543
|
-
<p
|
|
544
|
-
|
|
972
|
+
<p v-if="ui.columns.includes('size')">
|
|
973
|
+
{{ ui.labels[active.kind === "symlink" ? "link" : active.kind] }} ·
|
|
974
|
+
{{ directoryLike(active) ? ui.labels.folder : size(active.size) }}
|
|
975
|
+
</p>
|
|
976
|
+
<p v-if="ui.columns.includes('modified')">
|
|
977
|
+
{{ formattedDate(active.modifiedAt, ui, ready) }}
|
|
978
|
+
</p>
|
|
545
979
|
<p v-if="active.link">{{ active.link.target }}</p>
|
|
546
980
|
</aside>
|
|
547
|
-
<p role="status" aria-live="polite">{{ busy ?
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
981
|
+
<p role="status" aria-live="polite">{{ busy ? ui.labels.loading : error }}</p>
|
|
982
|
+
<button v-if="failure && failure !== 'copy'" :disabled="busy" @click="retry">
|
|
983
|
+
{{ ui.labels.retry }}
|
|
984
|
+
</button>
|
|
985
|
+
<a v-if="failure === 'folder' && retryRequest" :href="retryRequest[1]">{{
|
|
986
|
+
ui.labels.openNative
|
|
987
|
+
}}</a>
|
|
988
|
+
<nav v-if="model.page?.count > 1 && !complete" :aria-label="ui.labels.pagination">
|
|
989
|
+
<a v-if="model.page.previousHref" :href="model.page.previousHref" rel="prev">{{
|
|
990
|
+
ui.labels.previous
|
|
991
|
+
}}</a>
|
|
992
|
+
{{ label(ui, "page", { page: model.page.number, pages: model.page.count }) }}
|
|
993
|
+
<a v-if="model.page.nextHref" :href="model.page.nextHref" rel="next">{{ ui.labels.next }}</a>
|
|
552
994
|
</nav>
|
|
553
995
|
</main>
|
|
554
|
-
<footer class="cw-bottom">
|
|
555
|
-
<div class="cw-key-help">
|
|
996
|
+
<footer class="cw-bottom" ref="footerElement">
|
|
997
|
+
<div :class="ui.brand.footerText || $slots.footer ? 'cw-custom-footer' : 'cw-key-help'">
|
|
556
998
|
<slot name="footer" :model="model"
|
|
557
|
-
|
|
999
|
+
><template v-if="ui.brand.footerText"
|
|
1000
|
+
><a v-if="ui.brand.footerUrl" :href="ui.brand.footerUrl">{{ ui.brand.footerText }}</a
|
|
1001
|
+
><span v-else>{{ ui.brand.footerText }}</span></template
|
|
1002
|
+
><template v-else>{{ ui.labels.crosswaveKeyboardHelp }}</template></slot
|
|
558
1003
|
>
|
|
559
1004
|
</div>
|
|
560
|
-
<div class="cw-controller" v-if="pad">
|
|
1005
|
+
<div class="cw-controller" v-if="pad">{{ ui.labels.controller }}</div>
|
|
561
1006
|
<label class="cw-appearance"
|
|
562
|
-
>
|
|
563
|
-
|
|
1007
|
+
>{{ ui.labels.color
|
|
1008
|
+
}}<select v-model="color" :disabled="!ready">
|
|
1009
|
+
<option v-for="name in Object.keys(colors)" :key="name" :value="name">
|
|
1010
|
+
{{ ui.labels[name] }}
|
|
1011
|
+
</option>
|
|
564
1012
|
</select></label
|
|
565
1013
|
><button class="cw-motion" :disabled="!ready" @click="paused = !paused">
|
|
566
|
-
{{ paused ?
|
|
1014
|
+
{{ paused ? ui.labels.resumeMotion : ui.labels.pauseMotion }}
|
|
567
1015
|
</button>
|
|
568
1016
|
</footer>
|
|
1017
|
+
<dialog
|
|
1018
|
+
v-if="archiveOpen"
|
|
1019
|
+
ref="archiveDialog"
|
|
1020
|
+
class="cw-archive"
|
|
1021
|
+
:aria-label="ui.labels.searchArchive"
|
|
1022
|
+
@close="archiveOpen = false"
|
|
1023
|
+
@keydown.esc.prevent.stop="archiveOpen = false"
|
|
1024
|
+
>
|
|
1025
|
+
<h2>{{ ui.labels.searchArchive }}</h2>
|
|
1026
|
+
<input
|
|
1027
|
+
ref="archiveInput"
|
|
1028
|
+
type="search"
|
|
1029
|
+
v-model="archiveQuery"
|
|
1030
|
+
:aria-label="ui.labels.archiveSearchInput"
|
|
1031
|
+
/>
|
|
1032
|
+
<p role="status">{{ archiveStatus }}</p>
|
|
1033
|
+
<button v-if="archiveFailed" @click="searchArchive">{{ ui.labels.retry }}</button>
|
|
1034
|
+
<ul>
|
|
1035
|
+
<li v-for="record in archiveRows" :key="record.path">
|
|
1036
|
+
<a
|
|
1037
|
+
v-if="record.href"
|
|
1038
|
+
:href="record.href"
|
|
1039
|
+
:target="record.exitsExplorer ? '_blank' : undefined"
|
|
1040
|
+
:rel="record.exitsExplorer ? 'noopener' : undefined"
|
|
1041
|
+
>{{ record.path }}</a
|
|
1042
|
+
>
|
|
1043
|
+
<span v-else>{{ record.path }}</span>
|
|
1044
|
+
</li>
|
|
1045
|
+
</ul>
|
|
1046
|
+
<button @click="archiveOpen = false">{{ ui.labels.close }}</button>
|
|
1047
|
+
</dialog>
|
|
569
1048
|
</template>
|