@dirwell/theme-crosswave 0.0.1-alpha.1 → 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 +32 -2
- package/dist/client.js +19 -19
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +702 -59
- package/dist/index.mjs.map +1 -1
- package/dist/model-VZ01cfyg.d.mts +172 -0
- package/dist/model.d.mts +1 -1
- package/dist/source/Crosswave.vue +405 -57
- 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 -1
- package/package.json +7 -3
- package/dist/model-D4-K54I9.d.mts +0 -53
|
@@ -16,18 +16,28 @@ import {
|
|
|
16
16
|
applyThemeMetadata,
|
|
17
17
|
restoreThemePage,
|
|
18
18
|
} from "./shared/navigation.mjs";
|
|
19
|
+
import { readView, createViewHistory } from "./shared/view-state.mjs";
|
|
19
20
|
import { entryIcon } from "./shared/icons.mjs";
|
|
20
|
-
import { directoryLike, size
|
|
21
|
+
import { directoryLike, size } from "./shared/records.mjs";
|
|
22
|
+
import {
|
|
23
|
+
resolveCustomization,
|
|
24
|
+
label,
|
|
25
|
+
formattedDate,
|
|
26
|
+
copyEntryAction,
|
|
27
|
+
publicDataValue,
|
|
28
|
+
} from "./shared/customization.mjs";
|
|
21
29
|
import { createWave } from "./wave.mjs";
|
|
22
30
|
const props = defineProps({ model: { type: Object, required: true } }),
|
|
23
31
|
model = shallowRef(restoreThemePage(props.model, Boolean(import.meta.hot))),
|
|
24
|
-
entries = shallowRef(model.value.entries)
|
|
32
|
+
entries = shallowRef(model.value.entries),
|
|
33
|
+
ui = computed(() => resolveCustomization(model.value.ui));
|
|
25
34
|
const query = ref(""),
|
|
26
|
-
category = ref(
|
|
35
|
+
category = ref(model.value.categories[0]?.id ?? null),
|
|
27
36
|
selected = ref(null),
|
|
28
37
|
ready = ref(false),
|
|
29
38
|
busy = ref(false),
|
|
30
39
|
error = ref(""),
|
|
40
|
+
failure = ref(null),
|
|
31
41
|
color = ref(props.model.color),
|
|
32
42
|
paused = ref(!props.model.backgroundMotion),
|
|
33
43
|
clock = ref(""),
|
|
@@ -35,14 +45,20 @@ const query = ref(""),
|
|
|
35
45
|
waveReady = ref(false),
|
|
36
46
|
pendingUpdates = ref(null),
|
|
37
47
|
canvas = ref(),
|
|
38
|
-
search = ref()
|
|
48
|
+
search = ref(),
|
|
49
|
+
listScroll = ref(),
|
|
50
|
+
footerElement = ref(),
|
|
51
|
+
scrollTop = ref(0),
|
|
52
|
+
viewportHeight = ref(400),
|
|
53
|
+
retryRequest = shallowRef(null),
|
|
54
|
+
heights = shallowRef(new Map());
|
|
39
55
|
const complete = computed(() => entries.value.length === model.value.total),
|
|
40
56
|
membership = (entry) =>
|
|
41
57
|
Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
|
|
42
58
|
const rows = computed(() =>
|
|
43
59
|
entries.value.filter(
|
|
44
60
|
(entry) =>
|
|
45
|
-
|
|
61
|
+
membership(entry).includes(category.value) &&
|
|
46
62
|
`${entry.name} ${entry.link?.target ?? ""}`
|
|
47
63
|
.toLocaleLowerCase()
|
|
48
64
|
.includes(query.value.trim().toLocaleLowerCase()),
|
|
@@ -50,9 +66,116 @@ const rows = computed(() =>
|
|
|
50
66
|
),
|
|
51
67
|
active = computed(() => rows.value.find((entry) => entry.id === selected.value) ?? rows.value[0]),
|
|
52
68
|
heading = computed(
|
|
53
|
-
() =>
|
|
69
|
+
() =>
|
|
70
|
+
model.value.categories.find((item) => item.id === category.value)?.label ??
|
|
71
|
+
ui.value.labels.allFiles,
|
|
54
72
|
);
|
|
55
73
|
const nodes = new Map();
|
|
74
|
+
const virtual = computed(() => ready.value && rows.value.length > 500);
|
|
75
|
+
const offsets = computed(() => {
|
|
76
|
+
const result = [0];
|
|
77
|
+
for (const entry of rows.value) result.push(result.at(-1) + (heights.value.get(entry.id) ?? 76));
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
const start = computed(() => {
|
|
81
|
+
if (!virtual.value) return 0;
|
|
82
|
+
const position = Math.max(0, scrollTop.value - (model.value.parentHref ? 76 : 0));
|
|
83
|
+
let low = 0,
|
|
84
|
+
high = rows.value.length;
|
|
85
|
+
while (low < high) {
|
|
86
|
+
const middle = (low + high) >> 1;
|
|
87
|
+
if (offsets.value[middle + 1] <= position) low = middle + 1;
|
|
88
|
+
else high = middle;
|
|
89
|
+
}
|
|
90
|
+
return Math.max(0, low - 5);
|
|
91
|
+
});
|
|
92
|
+
const end = computed(() => {
|
|
93
|
+
if (!virtual.value) return rows.value.length;
|
|
94
|
+
let low = 0,
|
|
95
|
+
high = rows.value.length;
|
|
96
|
+
while (low < high) {
|
|
97
|
+
const middle = (low + high) >> 1;
|
|
98
|
+
if (offsets.value[middle + 1] <= scrollTop.value + viewportHeight.value) low = middle + 1;
|
|
99
|
+
else high = middle;
|
|
100
|
+
}
|
|
101
|
+
return Math.min(rows.value.length, Math.max(start.value + 35, low + 5));
|
|
102
|
+
});
|
|
103
|
+
const visibleRows = computed(() => rows.value.slice(start.value, end.value));
|
|
104
|
+
const heightCache = new Map();
|
|
105
|
+
let rowObserver, layoutObserver, viewHistory, measuredWidth;
|
|
106
|
+
function measureLayout() {
|
|
107
|
+
if (!listScroll.value) return;
|
|
108
|
+
const width = listScroll.value.clientWidth;
|
|
109
|
+
if (measuredWidth !== undefined && width !== measuredWidth) {
|
|
110
|
+
heightCache.clear();
|
|
111
|
+
heights.value = new Map(
|
|
112
|
+
[...nodes].map(([id, node]) => [id, node.getBoundingClientRect().height]),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
measuredWidth = width;
|
|
116
|
+
const footerHeight = footerElement.value?.getBoundingClientRect().height ?? 82;
|
|
117
|
+
document.documentElement.style.setProperty("--cw-footer-height", footerHeight + "px");
|
|
118
|
+
const available = Math.max(
|
|
119
|
+
160,
|
|
120
|
+
innerHeight - listScroll.value.getBoundingClientRect().top - footerHeight - 24,
|
|
121
|
+
);
|
|
122
|
+
viewportHeight.value = available;
|
|
123
|
+
listScroll.value.style.setProperty("--cw-list-height", available + "px");
|
|
124
|
+
}
|
|
125
|
+
watch([virtual, () => model.value.path], () => nextTick(measureLayout));
|
|
126
|
+
const viewDefaults = (page) => ({ query: "", category: page.categories[0]?.id ?? null });
|
|
127
|
+
function saveView() {
|
|
128
|
+
if (!viewHistory) return;
|
|
129
|
+
viewHistory.save(
|
|
130
|
+
new URL(model.value.dataHref, document.baseURI).href,
|
|
131
|
+
model.value.path,
|
|
132
|
+
{
|
|
133
|
+
query: query.value,
|
|
134
|
+
category: category.value,
|
|
135
|
+
selected: selected.value,
|
|
136
|
+
windowY: window.scrollY,
|
|
137
|
+
listTop: listScroll.value?.scrollTop ?? 0,
|
|
138
|
+
},
|
|
139
|
+
viewDefaults(model.value),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
function retry() {
|
|
143
|
+
if (failure.value === "folder" && retryRequest.value) openFolder(...retryRequest.value, true);
|
|
144
|
+
else openFolder(model.value.dataHref, location.href, false, true);
|
|
145
|
+
}
|
|
146
|
+
watchEffect((onCleanup) => {
|
|
147
|
+
if (!ready.value || busy.value) return;
|
|
148
|
+
const view = {
|
|
149
|
+
query: query.value,
|
|
150
|
+
category: category.value,
|
|
151
|
+
selected: selected.value,
|
|
152
|
+
listTop: scrollTop.value,
|
|
153
|
+
};
|
|
154
|
+
const dataHref = model.value.dataHref,
|
|
155
|
+
path = model.value.path,
|
|
156
|
+
defaults = viewDefaults(model.value);
|
|
157
|
+
const frame = requestAnimationFrame(() =>
|
|
158
|
+
viewHistory?.save(
|
|
159
|
+
new URL(dataHref, document.baseURI).href,
|
|
160
|
+
path,
|
|
161
|
+
{ ...view, windowY: window.scrollY },
|
|
162
|
+
defaults,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
onCleanup(() => cancelAnimationFrame(frame));
|
|
166
|
+
});
|
|
167
|
+
function setNode(id, node) {
|
|
168
|
+
const old = nodes.get(id);
|
|
169
|
+
if (old && old !== node) rowObserver?.unobserve(old);
|
|
170
|
+
if (node) {
|
|
171
|
+
nodes.set(id, node);
|
|
172
|
+
rowObserver?.observe(node);
|
|
173
|
+
} else nodes.delete(id);
|
|
174
|
+
}
|
|
175
|
+
watch([query, category], () => {
|
|
176
|
+
scrollTop.value = 0;
|
|
177
|
+
if (listScroll.value) listScroll.value.scrollTop = 0;
|
|
178
|
+
});
|
|
56
179
|
let wave,
|
|
57
180
|
gamepad,
|
|
58
181
|
abort,
|
|
@@ -84,45 +207,95 @@ async function loadComplete() {
|
|
|
84
207
|
});
|
|
85
208
|
if (token === navigation && !local.signal.aborted && !disposed) entries.value = data.entries;
|
|
86
209
|
} catch (cause) {
|
|
87
|
-
if (token === navigation && !local.signal.aborted)
|
|
88
|
-
error.value =
|
|
210
|
+
if (token === navigation && !local.signal.aborted) {
|
|
211
|
+
error.value = ui.value.labels.listingError;
|
|
212
|
+
failure.value = "listing";
|
|
213
|
+
}
|
|
89
214
|
} finally {
|
|
90
215
|
if (token === navigation) busy.value = false;
|
|
91
216
|
}
|
|
92
217
|
}
|
|
93
|
-
async function openFolder(
|
|
218
|
+
async function openFolder(
|
|
219
|
+
dataHref,
|
|
220
|
+
url,
|
|
221
|
+
push,
|
|
222
|
+
refresh = false,
|
|
223
|
+
restoreState = null,
|
|
224
|
+
fresh = false,
|
|
225
|
+
) {
|
|
226
|
+
if (push) saveView();
|
|
94
227
|
const token = ++navigation;
|
|
95
228
|
abort?.abort();
|
|
96
229
|
const local = (abort = new AbortController());
|
|
97
230
|
busy.value = true;
|
|
98
231
|
error.value = "";
|
|
232
|
+
failure.value = null;
|
|
99
233
|
try {
|
|
100
|
-
const next = await fetchThemePage(dataHref, url, "crosswave", local.signal, refresh);
|
|
234
|
+
const next = await fetchThemePage(dataHref, url, "crosswave", local.signal, refresh || fresh);
|
|
101
235
|
let nextEntries = next.entries;
|
|
102
236
|
if (refresh && next.manifestHref && nextEntries.length !== next.total)
|
|
103
237
|
nextEntries = (
|
|
104
238
|
await loadListing(next.manifestHref, { signal: local.signal, cache: "no-store" })
|
|
105
239
|
).entries;
|
|
106
240
|
if (token !== navigation || local.signal.aborted || disposed) return;
|
|
241
|
+
if (!refresh) viewHistory.commit(next.dataHref, next.path, url, push);
|
|
242
|
+
if (next.path !== model.value.path) {
|
|
243
|
+
heightCache.set(model.value.path, heights.value);
|
|
244
|
+
if (heightCache.size > 8) heightCache.delete(heightCache.keys().next().value);
|
|
245
|
+
heights.value = heightCache.get(next.path) ?? new Map();
|
|
246
|
+
}
|
|
107
247
|
model.value = next;
|
|
248
|
+
retryRequest.value = null;
|
|
108
249
|
entries.value = nextEntries;
|
|
109
250
|
if (!refresh) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
251
|
+
const view = readView(
|
|
252
|
+
url,
|
|
253
|
+
"crosswave",
|
|
254
|
+
next.path,
|
|
255
|
+
viewDefaults(next),
|
|
256
|
+
next.categories.map((item) => item.id),
|
|
257
|
+
restoreState,
|
|
258
|
+
);
|
|
259
|
+
query.value = view.query;
|
|
260
|
+
selected.value = view.selected;
|
|
261
|
+
category.value = view.category;
|
|
113
262
|
}
|
|
114
|
-
if (push) history.pushState({ dataHref: next.dataHref }, "", url);
|
|
115
263
|
applyThemeMetadata(next, url);
|
|
116
264
|
await loadComplete();
|
|
265
|
+
if (!refresh) {
|
|
266
|
+
const view = readView(
|
|
267
|
+
url,
|
|
268
|
+
"crosswave",
|
|
269
|
+
next.path,
|
|
270
|
+
viewDefaults(next),
|
|
271
|
+
next.categories.map((item) => item.id),
|
|
272
|
+
restoreState,
|
|
273
|
+
);
|
|
274
|
+
await nextTick();
|
|
275
|
+
if (selected.value && !rows.value.some((entry) => entry.id === selected.value))
|
|
276
|
+
selected.value = null;
|
|
277
|
+
scrollTop.value = view.listTop;
|
|
278
|
+
if (listScroll.value) listScroll.value.scrollTop = view.listTop;
|
|
279
|
+
await nextTick();
|
|
280
|
+
window.scrollTo({ top: view.windowY });
|
|
281
|
+
if (restoreState && selected.value)
|
|
282
|
+
nodes.get(selected.value)?.querySelector("a")?.focus({ preventScroll: true });
|
|
283
|
+
}
|
|
117
284
|
} catch (cause) {
|
|
118
|
-
if (token === navigation && !local.signal.aborted)
|
|
119
|
-
error.value =
|
|
285
|
+
if (token === navigation && !local.signal.aborted) {
|
|
286
|
+
error.value = ui.value.labels.folderError;
|
|
287
|
+
failure.value = "folder";
|
|
288
|
+
retryRequest.value = [dataHref, url, push, refresh, restoreState];
|
|
289
|
+
viewHistory?.rollback();
|
|
290
|
+
}
|
|
120
291
|
} finally {
|
|
121
292
|
if (token === navigation) busy.value = false;
|
|
122
293
|
}
|
|
123
294
|
}
|
|
124
295
|
function navigateLink(event, href, dataHref) {
|
|
125
296
|
if (
|
|
297
|
+
typeof href !== "string" ||
|
|
298
|
+
typeof dataHref !== "string" ||
|
|
126
299
|
event?.altKey ||
|
|
127
300
|
event?.ctrlKey ||
|
|
128
301
|
event?.metaKey ||
|
|
@@ -135,6 +308,7 @@ function navigateLink(event, href, dataHref) {
|
|
|
135
308
|
openFolder(dataHref, new URL(href, document.baseURI).href, true);
|
|
136
309
|
}
|
|
137
310
|
async function openEntry(event, entry) {
|
|
311
|
+
saveView();
|
|
138
312
|
const href = entry.presentation?.folderDataHref;
|
|
139
313
|
if (
|
|
140
314
|
entry.exitsExplorer ||
|
|
@@ -151,28 +325,41 @@ async function openEntry(event, entry) {
|
|
|
151
325
|
await openFolder(href, new URL(entry.href, document.baseURI).href, true);
|
|
152
326
|
}
|
|
153
327
|
async function move(index) {
|
|
154
|
-
const
|
|
328
|
+
const target = Math.max(0, Math.min(rows.value.length - 1, index));
|
|
329
|
+
const entry = rows.value[target];
|
|
155
330
|
if (!entry) return;
|
|
156
331
|
selected.value = entry.id;
|
|
332
|
+
if (virtual.value) {
|
|
333
|
+
scrollTop.value = Math.max(0, offsets.value[target] - 100 + (model.value.parentHref ? 76 : 0));
|
|
334
|
+
if (listScroll.value) listScroll.value.scrollTop = scrollTop.value;
|
|
335
|
+
}
|
|
157
336
|
await nextTick();
|
|
158
337
|
const node = nodes.get(entry.id);
|
|
159
338
|
node?.querySelector("a")?.focus({ preventScroll: true });
|
|
160
339
|
node?.scrollIntoView({ block: "nearest" });
|
|
161
340
|
}
|
|
162
|
-
function stepCategory(delta) {
|
|
341
|
+
async function stepCategory(delta) {
|
|
342
|
+
if (ui.value.direction === "rtl") delta = -delta;
|
|
343
|
+
const moveFocus = document.activeElement?.getAttribute("role") === "tab";
|
|
163
344
|
const items = model.value.categories,
|
|
164
345
|
index = items.findIndex((item) => item.id === category.value);
|
|
165
346
|
category.value = items[(index + delta + items.length) % items.length].id;
|
|
166
347
|
selected.value = null;
|
|
348
|
+
if (moveFocus) {
|
|
349
|
+
await nextTick();
|
|
350
|
+
document.getElementById("cw-tab-" + category.value)?.focus({ preventScroll: true });
|
|
351
|
+
}
|
|
167
352
|
}
|
|
168
|
-
function confirm() {
|
|
353
|
+
async function confirm() {
|
|
169
354
|
if (!active.value || busy.value) return;
|
|
355
|
+
await move(rows.value.findIndex((entry) => entry.id === active.value.id));
|
|
170
356
|
const anchor = nodes.get(active.value.id)?.querySelector("a");
|
|
171
357
|
anchor?.click();
|
|
172
358
|
}
|
|
173
359
|
function parent() {
|
|
174
360
|
if (model.value.parentHref) {
|
|
175
|
-
if (location.protocol === "file:"
|
|
361
|
+
if (location.protocol === "file:" || typeof model.value.parentDataHref !== "string")
|
|
362
|
+
location.href = model.value.parentHref;
|
|
176
363
|
else
|
|
177
364
|
openFolder(
|
|
178
365
|
model.value.parentDataHref,
|
|
@@ -183,6 +370,7 @@ function parent() {
|
|
|
183
370
|
}
|
|
184
371
|
function keyboard(event) {
|
|
185
372
|
if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
373
|
+
if (event.target?.closest?.("[data-dirwell-actions]")) return;
|
|
186
374
|
const editing = /INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "");
|
|
187
375
|
if (editing) {
|
|
188
376
|
if (event.key === "Escape") query.value = "";
|
|
@@ -276,7 +464,7 @@ const colors = {
|
|
|
276
464
|
};
|
|
277
465
|
const onPop = (event) =>
|
|
278
466
|
event.state?.dataHref
|
|
279
|
-
? openFolder(event.state.dataHref, location.href, false)
|
|
467
|
+
? openFolder(event.state.dataHref, location.href, false, false, event.state)
|
|
280
468
|
: location.reload();
|
|
281
469
|
const suspend = () => {
|
|
282
470
|
gamepad?.pause();
|
|
@@ -298,7 +486,43 @@ onMounted(() => {
|
|
|
298
486
|
ready.value = true;
|
|
299
487
|
document.body.setAttribute("data-cw-enhanced", "");
|
|
300
488
|
abort = new AbortController();
|
|
301
|
-
|
|
489
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
490
|
+
rowObserver = new ResizeObserver((observations) => {
|
|
491
|
+
const next = new Map(heights.value);
|
|
492
|
+
let changed = false;
|
|
493
|
+
for (const observation of observations) {
|
|
494
|
+
const id = observation.target.dataset.id;
|
|
495
|
+
const height = observation.target.getBoundingClientRect().height;
|
|
496
|
+
if (height > 0 && next.get(id) !== height) {
|
|
497
|
+
next.set(id, height);
|
|
498
|
+
changed = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (changed) heights.value = next;
|
|
502
|
+
});
|
|
503
|
+
for (const node of nodes.values()) rowObserver.observe(node);
|
|
504
|
+
layoutObserver = new ResizeObserver(measureLayout);
|
|
505
|
+
if (footerElement.value) layoutObserver.observe(footerElement.value);
|
|
506
|
+
const header = document.querySelector("[data-dirwell-theme='crosswave'] .cw-top");
|
|
507
|
+
if (header) layoutObserver.observe(header);
|
|
508
|
+
}
|
|
509
|
+
measureLayout();
|
|
510
|
+
window.addEventListener("resize", measureLayout);
|
|
511
|
+
viewHistory = createViewHistory("crosswave");
|
|
512
|
+
const view = readView(
|
|
513
|
+
location.href,
|
|
514
|
+
"crosswave",
|
|
515
|
+
model.value.path,
|
|
516
|
+
viewDefaults(model.value),
|
|
517
|
+
model.value.categories.map((item) => item.id),
|
|
518
|
+
history.state,
|
|
519
|
+
);
|
|
520
|
+
query.value = view.query;
|
|
521
|
+
category.value = view.category;
|
|
522
|
+
selected.value = view.selected;
|
|
523
|
+
scrollTop.value = view.listTop;
|
|
524
|
+
if (listScroll.value) listScroll.value.scrollTop = view.listTop;
|
|
525
|
+
saveView();
|
|
302
526
|
try {
|
|
303
527
|
const stored = localStorage.getItem("dirwell-crosswave-color");
|
|
304
528
|
if (Object.hasOwn(colors, stored)) color.value = stored;
|
|
@@ -347,7 +571,11 @@ onMounted(() => {
|
|
|
347
571
|
);
|
|
348
572
|
}
|
|
349
573
|
const updateClock = () =>
|
|
350
|
-
(clock.value = new Date().toLocaleTimeString(
|
|
574
|
+
(clock.value = new Date().toLocaleTimeString(ui.value.dateOptions.locale, {
|
|
575
|
+
timeZone: ui.value.dateOptions.timeZone,
|
|
576
|
+
hour: "2-digit",
|
|
577
|
+
minute: "2-digit",
|
|
578
|
+
}));
|
|
351
579
|
updateClock();
|
|
352
580
|
timer = setInterval(updateClock, 1000);
|
|
353
581
|
window.addEventListener("keydown", keyboard);
|
|
@@ -368,9 +596,15 @@ watch(
|
|
|
368
596
|
if (!href || typeof EventSource === "undefined") return;
|
|
369
597
|
const updates = new EventSource(href);
|
|
370
598
|
updates.onmessage = () => (pendingUpdates.value = href);
|
|
371
|
-
updates.onerror = () =>
|
|
599
|
+
updates.onerror = () => {
|
|
600
|
+
error.value = ui.value.labels.updatesError;
|
|
601
|
+
failure.value = "updates";
|
|
602
|
+
};
|
|
372
603
|
updates.onopen = () => {
|
|
373
|
-
if (
|
|
604
|
+
if (failure.value === "updates") {
|
|
605
|
+
error.value = "";
|
|
606
|
+
failure.value = null;
|
|
607
|
+
}
|
|
374
608
|
};
|
|
375
609
|
onCleanup(() => updates.close());
|
|
376
610
|
},
|
|
@@ -397,6 +631,9 @@ onBeforeUnmount(() => {
|
|
|
397
631
|
disposed = true;
|
|
398
632
|
document.body.removeAttribute("data-cw-enhanced");
|
|
399
633
|
abort?.abort();
|
|
634
|
+
rowObserver?.disconnect();
|
|
635
|
+
layoutObserver?.disconnect();
|
|
636
|
+
window.removeEventListener("resize", measureLayout);
|
|
400
637
|
gamepad?.pause();
|
|
401
638
|
wave?.dispose();
|
|
402
639
|
clearInterval(timer);
|
|
@@ -418,16 +655,24 @@ onBeforeUnmount(() => {
|
|
|
418
655
|
<header class="cw-top">
|
|
419
656
|
<div class="cw-brand">
|
|
420
657
|
<slot name="header" :model="model">
|
|
658
|
+
<img
|
|
659
|
+
v-if="ui.brand.logoUrl"
|
|
660
|
+
:src="ui.brand.logoUrl"
|
|
661
|
+
:alt="ui.brand.logoAlt ?? ''"
|
|
662
|
+
width="40"
|
|
663
|
+
height="40"
|
|
664
|
+
/>
|
|
421
665
|
<a
|
|
422
666
|
:href="model.rootHref"
|
|
423
667
|
@click="navigateLink($event, model.rootHref, model.rootDataHref)"
|
|
424
|
-
>{{ model.title }}</a
|
|
668
|
+
>{{ ui.brand.name ?? model.title }}</a
|
|
425
669
|
></slot
|
|
426
670
|
>
|
|
427
671
|
</div>
|
|
428
|
-
<nav class="cw-location" aria-label="
|
|
429
|
-
<a :href="model.rootHref" @click="navigateLink($event, model.rootHref, model.rootDataHref)"
|
|
430
|
-
|
|
672
|
+
<nav class="cw-location" :aria-label="ui.labels.breadcrumb">
|
|
673
|
+
<a :href="model.rootHref" @click="navigateLink($event, model.rootHref, model.rootDataHref)">{{
|
|
674
|
+
ui.labels.home
|
|
675
|
+
}}</a
|
|
431
676
|
><template v-for="crumb in model.breadcrumbs" :key="crumb.href">
|
|
432
677
|
/
|
|
433
678
|
<a :href="crumb.href" @click="navigateLink($event, crumb.href, crumb.dataHref)">{{
|
|
@@ -438,8 +683,8 @@ onBeforeUnmount(() => {
|
|
|
438
683
|
<div class="cw-clock" v-if="ready">{{ clock }}</div>
|
|
439
684
|
</header>
|
|
440
685
|
<main class="cw-stage" data-framework="vue" :aria-busy="busy">
|
|
441
|
-
<h1 class="cw-sr">
|
|
442
|
-
<nav class="cw-rail" aria-label="
|
|
686
|
+
<h1 class="cw-sr">{{ label(ui, "filesIn", { path: model.path || "/" }) }}</h1>
|
|
687
|
+
<nav class="cw-rail" :aria-label="ui.labels.categories" :hidden="!ready">
|
|
443
688
|
<div class="cw-categories" role="tablist">
|
|
444
689
|
<button
|
|
445
690
|
v-for="item in model.categories"
|
|
@@ -472,13 +717,21 @@ onBeforeUnmount(() => {
|
|
|
472
717
|
<div class="cw-list-head">
|
|
473
718
|
<div class="cw-directory">
|
|
474
719
|
<h2>{{ heading }}</h2>
|
|
475
|
-
<span data-cw-count>{{
|
|
720
|
+
<span data-cw-count>{{
|
|
721
|
+
label(ui, "items", { count: complete ? rows.length : entries.length })
|
|
722
|
+
}}</span>
|
|
476
723
|
</div>
|
|
477
724
|
<label class="cw-search"
|
|
478
|
-
>
|
|
725
|
+
><span>{{ ui.labels.search }}</span
|
|
726
|
+
><input ref="search" type="search" v-model="query" :disabled="!ready || !complete"
|
|
479
727
|
/></label>
|
|
480
728
|
</div>
|
|
481
|
-
<div
|
|
729
|
+
<div
|
|
730
|
+
class="cw-list-scroll"
|
|
731
|
+
:data-virtual="virtual"
|
|
732
|
+
ref="listScroll"
|
|
733
|
+
@scroll="scrollTop = $event.currentTarget.scrollTop"
|
|
734
|
+
>
|
|
482
735
|
<ul class="cw-files">
|
|
483
736
|
<li v-if="model.parentHref" class="cw-row">
|
|
484
737
|
<a
|
|
@@ -488,12 +741,16 @@ onBeforeUnmount(() => {
|
|
|
488
741
|
>../</a
|
|
489
742
|
>
|
|
490
743
|
</li>
|
|
744
|
+
<li v-if="start" aria-hidden="true" :style="{ height: offsets[start] + 'px' }"></li>
|
|
491
745
|
<li
|
|
492
|
-
v-for="entry in
|
|
746
|
+
v-for="(entry, visibleIndex) in visibleRows"
|
|
493
747
|
:key="entry.id"
|
|
494
|
-
:ref="(node) => (
|
|
748
|
+
:ref="(node) => setNode(entry.id, node)"
|
|
749
|
+
:aria-posinset="start + visibleIndex + 1"
|
|
750
|
+
:aria-setsize="rows.length"
|
|
495
751
|
:class="['cw-row', { 'cw-selected': active?.id === entry.id }]"
|
|
496
752
|
data-cw-entry
|
|
753
|
+
:data-custom-columns="Boolean(model.ui?.columns)"
|
|
497
754
|
:data-id="entry.id"
|
|
498
755
|
>
|
|
499
756
|
<a
|
|
@@ -519,13 +776,30 @@ onBeforeUnmount(() => {
|
|
|
519
776
|
"
|
|
520
777
|
></span>
|
|
521
778
|
<span class="cw-entry-label">
|
|
522
|
-
<
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
779
|
+
<template
|
|
780
|
+
v-for="column in ui.columns"
|
|
781
|
+
:key="typeof column === 'string' ? column : column.dataKey"
|
|
782
|
+
><span v-if="column === 'name'" class="cw-name"
|
|
783
|
+
>{{ entry.name }}{{ directoryLike(entry) ? "/" : "" }}</span
|
|
784
|
+
>
|
|
785
|
+
<span v-if="entry.link && column === 'name'" class="cw-target">{{
|
|
786
|
+
entry.link.target
|
|
787
|
+
}}</span>
|
|
788
|
+
<span v-if="column === 'size'" class="cw-row-meta"
|
|
789
|
+
>{{
|
|
790
|
+
entry.link?.unavailable
|
|
791
|
+
? ui.labels.unavailable
|
|
792
|
+
: ui.labels[entry.kind === "symlink" ? "link" : entry.kind]
|
|
793
|
+
}}
|
|
794
|
+
· {{ directoryLike(entry) ? ui.labels.folder : size(entry.size) }}</span
|
|
795
|
+
>
|
|
796
|
+
<span v-if="column === 'modified'" class="cw-row-date"
|
|
797
|
+
>{{ ui.labels.modified }}
|
|
798
|
+
{{ formattedDate(entry.modifiedAt, ui, ready) }}</span
|
|
799
|
+
><span v-if="typeof column === 'object'" class="cw-data-value"
|
|
800
|
+
>{{ column.label }}: {{ publicDataValue(entry.data?.[column.dataKey]) }}</span
|
|
801
|
+
></template
|
|
527
802
|
>
|
|
528
|
-
<span class="cw-row-date">Modified {{ modified(entry.modifiedAt) }}</span>
|
|
529
803
|
</span></slot
|
|
530
804
|
></a
|
|
531
805
|
><span v-else class="cw-entry cw-unavailable"
|
|
@@ -533,37 +807,111 @@ onBeforeUnmount(() => {
|
|
|
533
807
|
entry.name
|
|
534
808
|
}}</slot></span
|
|
535
809
|
>
|
|
810
|
+
<span
|
|
811
|
+
v-if="ui.actions.length || $slots.actions"
|
|
812
|
+
data-dirwell-actions
|
|
813
|
+
class="cw-row-actions"
|
|
814
|
+
>
|
|
815
|
+
<slot
|
|
816
|
+
name="actions"
|
|
817
|
+
:model="model"
|
|
818
|
+
:entry="entry"
|
|
819
|
+
:selected="selected === entry.id"
|
|
820
|
+
/>
|
|
821
|
+
<template v-for="action in ui.actions" :key="action">
|
|
822
|
+
<a
|
|
823
|
+
v-if="
|
|
824
|
+
action === 'download' &&
|
|
825
|
+
entry.href &&
|
|
826
|
+
!directoryLike(entry) &&
|
|
827
|
+
!entry.link?.unavailable
|
|
828
|
+
"
|
|
829
|
+
:href="safeHref(entry)"
|
|
830
|
+
download
|
|
831
|
+
>{{ ui.labels.download }}</a
|
|
832
|
+
>
|
|
833
|
+
<button
|
|
834
|
+
v-else-if="action !== 'download'"
|
|
835
|
+
:disabled="!ready || (action === 'copy-link' && !entry.href)"
|
|
836
|
+
:aria-label="
|
|
837
|
+
(action === 'copy-link' ? ui.labels.copyLink : ui.labels.copyPath) +
|
|
838
|
+
': ' +
|
|
839
|
+
entry.name
|
|
840
|
+
"
|
|
841
|
+
@click="
|
|
842
|
+
async () => {
|
|
843
|
+
try {
|
|
844
|
+
await copyEntryAction(action, entry);
|
|
845
|
+
error = ui.labels.copied;
|
|
846
|
+
failure = null;
|
|
847
|
+
} catch {
|
|
848
|
+
error = ui.labels.copyError;
|
|
849
|
+
failure = 'copy';
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
"
|
|
853
|
+
>
|
|
854
|
+
{{ action === "copy-link" ? ui.labels.copyLink : ui.labels.copyPath }}
|
|
855
|
+
</button>
|
|
856
|
+
</template>
|
|
857
|
+
</span>
|
|
536
858
|
</li>
|
|
859
|
+
<li
|
|
860
|
+
v-if="end < rows.length"
|
|
861
|
+
aria-hidden="true"
|
|
862
|
+
:style="{ height: offsets[rows.length] - offsets[end] + 'px' }"
|
|
863
|
+
></li>
|
|
537
864
|
</ul>
|
|
538
|
-
<p class="cw-empty" v-if="!rows.length
|
|
865
|
+
<p class="cw-empty" v-if="!rows.length && !busy">
|
|
866
|
+
{{ model.total === 0 ? ui.labels.empty : ui.labels.noMatches }}
|
|
867
|
+
</p>
|
|
539
868
|
</div>
|
|
540
869
|
</section>
|
|
541
870
|
<aside class="cw-detail" v-if="ready && active">
|
|
542
871
|
<h2>{{ active.name }}</h2>
|
|
543
|
-
<p
|
|
544
|
-
|
|
872
|
+
<p v-if="ui.columns.includes('size')">
|
|
873
|
+
{{ ui.labels[active.kind === "symlink" ? "link" : active.kind] }} ·
|
|
874
|
+
{{ directoryLike(active) ? ui.labels.folder : size(active.size) }}
|
|
875
|
+
</p>
|
|
876
|
+
<p v-if="ui.columns.includes('modified')">
|
|
877
|
+
{{ formattedDate(active.modifiedAt, ui, ready) }}
|
|
878
|
+
</p>
|
|
545
879
|
<p v-if="active.link">{{ active.link.target }}</p>
|
|
546
880
|
</aside>
|
|
547
|
-
<p role="status" aria-live="polite">{{ busy ?
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
881
|
+
<p role="status" aria-live="polite">{{ busy ? ui.labels.loading : error }}</p>
|
|
882
|
+
<button v-if="failure && failure !== 'copy'" :disabled="busy" @click="retry">
|
|
883
|
+
{{ ui.labels.retry }}
|
|
884
|
+
</button>
|
|
885
|
+
<a v-if="failure === 'folder' && retryRequest" :href="retryRequest[1]">{{
|
|
886
|
+
ui.labels.openNative
|
|
887
|
+
}}</a>
|
|
888
|
+
<nav v-if="model.page?.count > 1 && !complete" :aria-label="ui.labels.pagination">
|
|
889
|
+
<a v-if="model.page.previousHref" :href="model.page.previousHref" rel="prev">{{
|
|
890
|
+
ui.labels.previous
|
|
891
|
+
}}</a>
|
|
892
|
+
{{ label(ui, "page", { page: model.page.number, pages: model.page.count }) }}
|
|
893
|
+
<a v-if="model.page.nextHref" :href="model.page.nextHref" rel="next">{{ ui.labels.next }}</a>
|
|
552
894
|
</nav>
|
|
553
895
|
</main>
|
|
554
|
-
<footer class="cw-bottom">
|
|
555
|
-
<div class="cw-key-help">
|
|
896
|
+
<footer class="cw-bottom" ref="footerElement">
|
|
897
|
+
<div :class="ui.brand.footerText || $slots.footer ? 'cw-custom-footer' : 'cw-key-help'">
|
|
556
898
|
<slot name="footer" :model="model"
|
|
557
|
-
|
|
899
|
+
><template v-if="ui.brand.footerText"
|
|
900
|
+
><a v-if="ui.brand.footerUrl" :href="ui.brand.footerUrl">{{ ui.brand.footerText }}</a
|
|
901
|
+
><span v-else>{{ ui.brand.footerText }}</span></template
|
|
902
|
+
><template v-else>{{ ui.labels.crosswaveKeyboardHelp }}</template></slot
|
|
558
903
|
>
|
|
559
904
|
</div>
|
|
560
|
-
<div class="cw-controller" v-if="pad">
|
|
905
|
+
<div class="cw-controller" v-if="pad">{{ ui.labels.controller }}</div>
|
|
561
906
|
<label class="cw-appearance"
|
|
562
|
-
>
|
|
563
|
-
|
|
907
|
+
>{{ ui.labels.color
|
|
908
|
+
}}<select v-model="color" :disabled="!ready">
|
|
909
|
+
<option v-for="name in Object.keys(colors)" :key="name" :value="name">
|
|
910
|
+
{{ ui.labels[name] }}
|
|
911
|
+
</option>
|
|
564
912
|
</select></label
|
|
565
913
|
><button class="cw-motion" :disabled="!ready" @click="paused = !paused">
|
|
566
|
-
{{ paused ?
|
|
914
|
+
{{ paused ? ui.labels.resumeMotion : ui.labels.pauseMotion }}
|
|
567
915
|
</button>
|
|
568
916
|
</footer>
|
|
569
917
|
</template>
|