@brftech/filex-core 0.41.0 → 0.41.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 +8 -0
- package/dist/{ArchiveViewer-BNqukFg8.js → ArchiveViewer-Dq1c4-G5.js} +2 -2
- package/dist/{ArchiveViewer-BNqukFg8.js.map → ArchiveViewer-Dq1c4-G5.js.map} +1 -1
- package/dist/{CsvViewer-ii_-MgmG.js → CsvViewer-CeHslAc0.js} +2 -2
- package/dist/{CsvViewer-ii_-MgmG.js.map → CsvViewer-CeHslAc0.js.map} +1 -1
- package/dist/{DrawioViewer-B2tuu4rX.js → DrawioViewer-p03u7Oda.js} +2 -2
- package/dist/{DrawioViewer-B2tuu4rX.js.map → DrawioViewer-p03u7Oda.js.map} +1 -1
- package/dist/{EpubViewer-p4B8iiWb.js → EpubViewer-D08Y-Tww.js} +2 -2
- package/dist/{EpubViewer-p4B8iiWb.js.map → EpubViewer-D08Y-Tww.js.map} +1 -1
- package/dist/{IpynbViewer-D_qYRJhu.js → IpynbViewer-mG7p3Sil.js} +2 -2
- package/dist/{IpynbViewer-D_qYRJhu.js.map → IpynbViewer-mG7p3Sil.js.map} +1 -1
- package/dist/{MermaidViewer-3ZkfNM8v.js → MermaidViewer-CCPg1Q3W.js} +2 -2
- package/dist/{MermaidViewer-3ZkfNM8v.js.map → MermaidViewer-CCPg1Q3W.js.map} +1 -1
- package/dist/{PsdViewer-CuV-OPSd.js → PsdViewer-CDEHTiUN.js} +2 -2
- package/dist/{PsdViewer-CuV-OPSd.js.map → PsdViewer-CDEHTiUN.js.map} +1 -1
- package/dist/{TiffViewer-BLeoF4Be.js → TiffViewer-DnWRUGOK.js} +2 -2
- package/dist/{TiffViewer-BLeoF4Be.js.map → TiffViewer-DnWRUGOK.js.map} +1 -1
- package/dist/{Viewer3D-DHhhU76E.js → Viewer3D-COCQp53D.js} +2 -2
- package/dist/{Viewer3D-DHhhU76E.js.map → Viewer3D-COCQp53D.js.map} +1 -1
- package/dist/filex-core.js +81 -80
- package/dist/filex-core.umd.cjs +47 -47
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-BraG7Cz4.js → index-JUiZzLmF.js} +10418 -10350
- package/dist/index-JUiZzLmF.js.map +1 -0
- package/dist/index.d.ts +41 -38
- package/package.json +1 -1
- package/src/components/FilePane.vue +46 -5
- package/src/components/GalleryView.vue +10 -26
- package/src/components/GridView.vue +16 -27
- package/src/components/ListView.vue +17 -29
- package/src/components/PendingOpsTray.vue +4 -5
- package/src/components/TimeZoneDialog.vue +18 -10
- package/src/components/TokensPanel.vue +20 -11
- package/src/composables/useExplorerTimeZone.ts +7 -3
- package/src/composables/useLocale.ts +31 -15
- package/src/composables/usePendingOps.ts +5 -0
- package/src/composables/useRowTouch.ts +126 -0
- package/src/index.ts +5 -0
- package/src/lib/opProgress.ts +43 -0
- package/src/lib/shareTtl.ts +21 -2
- package/src/lib/timezone.ts +46 -1
- package/src/locales/en.ts +2 -0
- package/src/locales/tr.ts +2 -0
- package/src/modals/PermissionsModal.vue +16 -5
- package/dist/index-BraG7Cz4.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -4614,7 +4614,9 @@ export declare function formatByteSize(bytes: number | null | undefined, opts?:
|
|
|
4614
4614
|
* chosen clock, together, in one call. Every date on every surface goes
|
|
4615
4615
|
* through here — that is the whole point of it being here.
|
|
4616
4616
|
*/
|
|
4617
|
-
export declare function formatInstant(d: Date, code: LocaleCode | string | undefined, opts: Intl.DateTimeFormatOptions
|
|
4617
|
+
export declare function formatInstant(d: Date, code: LocaleCode | string | undefined, opts: Intl.DateTimeFormatOptions,
|
|
4618
|
+
/** The explorer whose clock applies (EXPLORER_CLOCK); omitted = page-wide. */
|
|
4619
|
+
owner?: symbol): string;
|
|
4618
4620
|
|
|
4619
4621
|
/**
|
|
4620
4622
|
* Format 20 raw bytes as the string the user writes down:
|
|
@@ -5261,6 +5263,37 @@ export declare type OperationsStore = ReturnType<typeof useOperations>;
|
|
|
5261
5263
|
|
|
5262
5264
|
export declare type OperationStatus = 'running' | 'done' | 'error' | 'aborted';
|
|
5263
5265
|
|
|
5266
|
+
/** 0–100, or `null` when there is no honest number to draw. */
|
|
5267
|
+
export declare function opPercent(op: OpProgressLike): number | null;
|
|
5268
|
+
|
|
5269
|
+
/**
|
|
5270
|
+
* opProgress — the one rule for "how far along is this queued operation".
|
|
5271
|
+
*
|
|
5272
|
+
* Both progress surfaces read it: the explorer's operations center
|
|
5273
|
+
* (PendingOpsTray → OperationsCenter) and the admin app's bottom-right tray.
|
|
5274
|
+
* Two copies of this rule are how one tray starts saying something the other
|
|
5275
|
+
* does not (filex lesson #119).
|
|
5276
|
+
*
|
|
5277
|
+
* Issue #27: a queued op counts SOURCES, so moving one large file — or one
|
|
5278
|
+
* folder of a thousand files — is "0 of 1" until the very end. Drawn as a
|
|
5279
|
+
* percentage that is a bar frozen at 0% that then vanishes, which the reporter
|
|
5280
|
+
* read as stuck. So:
|
|
5281
|
+
*
|
|
5282
|
+
* - a running cross-storage transfer reports bytes (`bytes_done` /
|
|
5283
|
+
* `bytes_total`); when the total is known that is the percentage;
|
|
5284
|
+
* - bytes moving with no total yet, or a single source with nothing but a
|
|
5285
|
+
* source count, has NO honest percentage: `null`, and the surface shows
|
|
5286
|
+
* an indeterminate indicator instead of a fake 0%;
|
|
5287
|
+
* - several sources still report per source, as before.
|
|
5288
|
+
*/
|
|
5289
|
+
export declare interface OpProgressLike {
|
|
5290
|
+
status: string;
|
|
5291
|
+
progress_total: number;
|
|
5292
|
+
progress_done: number;
|
|
5293
|
+
bytes_total?: number;
|
|
5294
|
+
bytes_done?: number;
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5264
5297
|
declare type PaneId = 'main' | 'split';
|
|
5265
5298
|
|
|
5266
5299
|
/**
|
|
@@ -5292,6 +5325,9 @@ export declare interface PendingOp {
|
|
|
5292
5325
|
status: 'pending' | 'running' | 'done' | 'error';
|
|
5293
5326
|
progress_total: number;
|
|
5294
5327
|
progress_done: number;
|
|
5328
|
+
/** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
|
|
5329
|
+
bytes_total?: number;
|
|
5330
|
+
bytes_done?: number;
|
|
5295
5331
|
target_path: string | null;
|
|
5296
5332
|
source_dir: string | null;
|
|
5297
5333
|
source_count: number;
|
|
@@ -6373,43 +6409,6 @@ export declare type TimeZoneSources = Partial<Record<Exclude<TimeZoneTier, 'devi
|
|
|
6373
6409
|
/** What each tier currently holds (`''` = nothing). Reactive. */
|
|
6374
6410
|
export declare function timeZoneSources(): Required<TimeZoneSources>;
|
|
6375
6411
|
|
|
6376
|
-
/**
|
|
6377
|
-
* timezone.ts — whose clock an instant is read on.
|
|
6378
|
-
*
|
|
6379
|
-
* A timestamp is an instant. `last_modified` is the same number for everybody
|
|
6380
|
-
* on the planet; what differs is the clock it is read against. The owner's
|
|
6381
|
-
* rule, verbatim: *"ben 03'te video yükledim, GMT 0 eleman o videonun yüklenme
|
|
6382
|
-
* saatini kendi zaman diliminde görecek."* So the instant stays untouched on
|
|
6383
|
-
* the wire and the READER picks the clock.
|
|
6384
|
-
*
|
|
6385
|
-
* ⚠⚠ WHICH reader is a question with several answers, and this module is the
|
|
6386
|
-
* ONE place they are ranked. Before it, the admin web app followed the account
|
|
6387
|
-
* and an embedded `<filex-explorer>` on another origin followed the browser —
|
|
6388
|
-
* a person who chose Tokyo saw Tokyo in one and their laptop's zone in the
|
|
6389
|
-
* other, for the same file. That disagreement is the bug this exists to end,
|
|
6390
|
-
* so every surface resolves through `resolveTimeZone()` and nothing else
|
|
6391
|
-
* decides.
|
|
6392
|
-
*
|
|
6393
|
-
* The owner's ruling (2026-09-14): three tiers — the embedder, then the
|
|
6394
|
-
* account behind the key when it IS a person's key, then the browser — plus a
|
|
6395
|
-
* setting inside the embed, kept in the browser, because an embed has no
|
|
6396
|
-
* user-settings dialog. Where that setting ranks is `TIME_ZONE_TIERS` below,
|
|
6397
|
-
* and the reasoning is on it.
|
|
6398
|
-
*
|
|
6399
|
-
* Scope: page-level, deliberately. The clock belongs to the person looking at
|
|
6400
|
-
* the page, and there is one of them. Host and account zones are REGISTERED by
|
|
6401
|
-
* whoever knows them (an explorer instance, the web app's auth store) under an
|
|
6402
|
-
* owner key, so an explorer that unmounts takes its zones with it instead of
|
|
6403
|
-
* leaving them behind for the next one. Two explorers on one page that
|
|
6404
|
-
* disagree about a host zone, or hold different people's tokens, resolve to
|
|
6405
|
-
* the most recently written value for both — one viewer, one clock.
|
|
6406
|
-
*
|
|
6407
|
-
* The same-tab `filex:timezone` event survives from the previous design and for
|
|
6408
|
-
* the same reason: the admin app and a `<filex-explorer>` web component on one
|
|
6409
|
-
* page can be two different BUNDLES, i.e. two copies of this module, and a ref
|
|
6410
|
-
* in one says nothing to the other. The event crosses that gap for the one tier
|
|
6411
|
-
* that is shared state by nature — the browser's.
|
|
6412
|
-
*/
|
|
6413
6412
|
/**
|
|
6414
6413
|
* The places a zone can come from.
|
|
6415
6414
|
*
|
|
@@ -7047,6 +7046,8 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
|
|
|
7047
7046
|
status: "pending" | "running" | "done" | "error";
|
|
7048
7047
|
progress_total: number;
|
|
7049
7048
|
progress_done: number;
|
|
7049
|
+
bytes_total?: number | undefined;
|
|
7050
|
+
bytes_done?: number | undefined;
|
|
7050
7051
|
target_path: string | null;
|
|
7051
7052
|
source_dir: string | null;
|
|
7052
7053
|
source_count: number;
|
|
@@ -7060,6 +7061,8 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
|
|
|
7060
7061
|
status: "pending" | "running" | "done" | "error";
|
|
7061
7062
|
progress_total: number;
|
|
7062
7063
|
progress_done: number;
|
|
7064
|
+
bytes_total?: number | undefined;
|
|
7065
|
+
bytes_done?: number | undefined;
|
|
7063
7066
|
target_path: string | null;
|
|
7064
7067
|
source_dir: string | null;
|
|
7065
7068
|
source_count: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-core",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.2",
|
|
4
4
|
"description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/filex-core.umd.cjs",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
import { computed, provide, ref, watch } from 'vue';
|
|
67
67
|
|
|
68
68
|
import type { FileApi } from '../composables/useFileApi';
|
|
69
|
+
import type { ClickMod } from '../composables/useRowTouch';
|
|
69
70
|
import type { FileNode, ViewMode } from '../types/FileNode';
|
|
70
71
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
71
72
|
import { useLocale } from '../composables/useLocale';
|
|
@@ -236,7 +237,7 @@ const emit = defineEmits<{
|
|
|
236
237
|
/** The virtual `.trash` row was opened — the trash view carries the restore
|
|
237
238
|
* actions and belongs to the main pane, so the host decides. */
|
|
238
239
|
(e: 'open-trash'): void;
|
|
239
|
-
(e: 'click-row', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
|
|
240
|
+
(e: 'click-row', node: FileNode, mod: { ctrl: boolean; shift: boolean; touch?: boolean }): void;
|
|
240
241
|
/** Right-click. `null` = empty space (nothing selected → "Paste" only). */
|
|
241
242
|
(e: 'context', node: FileNode | null, ev: MouseEvent): void;
|
|
242
243
|
(e: 'clear-selection'): void;
|
|
@@ -443,11 +444,51 @@ const paneSubfolders = computed(() =>
|
|
|
443
444
|
* `useSelection` instance for this pane answers. One set of Ctrl/Shift
|
|
444
445
|
* semantics for both panes — the old right pane had a simplified copy in
|
|
445
446
|
* which Shift behaved as Ctrl. */
|
|
446
|
-
|
|
447
|
+
/* issue #26, second round — a press on the NAME opens, so the second click of
|
|
448
|
+
* a habitual double-click lands on whatever the NEW listing put under the
|
|
449
|
+
* pointer (the folder already opened on the first click). Without this guard
|
|
450
|
+
* that click would open or select a row the user never aimed at, and a file's
|
|
451
|
+
* name would open twice (two viewer tabs). */
|
|
452
|
+
const NAME_OPEN_GUARD_MS = 500;
|
|
453
|
+
let nameOpenedAt = 0;
|
|
454
|
+
|
|
455
|
+
function withinNameOpenGuard(): boolean {
|
|
456
|
+
return Date.now() - nameOpenedAt < NAME_OPEN_GUARD_MS;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function onViewClick(n: FileNode, mod: ClickMod) {
|
|
460
|
+
if (withinNameOpenGuard()) return;
|
|
461
|
+
/* issue #26, second round — the reporter's rule on every device: a press on
|
|
462
|
+
* the item's name opens it, mouse or finger, selection or not. Ctrl/shift
|
|
463
|
+
* still mean "add to / extend the selection", so a modifier click on a name
|
|
464
|
+
* selects as it always did. The checkbox selects on its own path. */
|
|
465
|
+
if (mod.name && !mod.ctrl && !mod.shift) {
|
|
466
|
+
nameOpenedAt = Date.now();
|
|
467
|
+
onRowOpen(n);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
/* issue #26 — a finger has no double-click, so a TAP elsewhere on the item
|
|
471
|
+
* is the open gesture too: with nothing selected it opens what it lands on,
|
|
472
|
+
* exactly as a double-click would. Once something is selected (a long press
|
|
473
|
+
* selects, via its menu) such a tap adds to or removes from the selection,
|
|
474
|
+
* so picking several files still works. A mouse click beside the name keeps
|
|
475
|
+
* click-to-select; see composables/useRowTouch. */
|
|
476
|
+
if (mod.touch && !mod.ctrl && !mod.shift) {
|
|
477
|
+
if (props.selected.size === 0) {
|
|
478
|
+
onRowOpen(n);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
mod = { ...mod, ctrl: true };
|
|
482
|
+
}
|
|
447
483
|
emit('activate');
|
|
448
484
|
emit('click-row', n, mod);
|
|
449
485
|
}
|
|
450
486
|
|
|
487
|
+
function onViewDbl(n: FileNode) {
|
|
488
|
+
if (withinNameOpenGuard()) return;
|
|
489
|
+
onRowOpen(n);
|
|
490
|
+
}
|
|
491
|
+
|
|
451
492
|
function onViewContext(n: FileNode, ev: MouseEvent) {
|
|
452
493
|
ev.preventDefault();
|
|
453
494
|
ev.stopPropagation();
|
|
@@ -879,7 +920,7 @@ watch(panePath, () => {
|
|
|
879
920
|
:auth-credentials="authCredentials"
|
|
880
921
|
@click-row="onViewClick"
|
|
881
922
|
@display-order="(nodes: FileNode[]) => emit('display-order', nodes)"
|
|
882
|
-
@dbl-row="
|
|
923
|
+
@dbl-row="onViewDbl"
|
|
883
924
|
@context-row="onViewContext"
|
|
884
925
|
@item-drag-start="onRowDragStart"
|
|
885
926
|
@item-drop-into="onViewDropInto"
|
|
@@ -905,7 +946,7 @@ watch(panePath, () => {
|
|
|
905
946
|
:auth-credentials="authCredentials"
|
|
906
947
|
@click-card="onViewClick"
|
|
907
948
|
@display-order="(nodes: FileNode[]) => emit('display-order', nodes)"
|
|
908
|
-
@dbl-card="
|
|
949
|
+
@dbl-card="onViewDbl"
|
|
909
950
|
@context-card="onViewContext"
|
|
910
951
|
@item-drag-start="onRowDragStart"
|
|
911
952
|
@item-drop-into="onViewDropInto"
|
|
@@ -928,7 +969,7 @@ watch(panePath, () => {
|
|
|
928
969
|
:auth-headers="authHeaders"
|
|
929
970
|
:auth-credentials="authCredentials"
|
|
930
971
|
@click-card="onViewClick"
|
|
931
|
-
@dbl-card="
|
|
972
|
+
@dbl-card="onViewDbl"
|
|
932
973
|
@context-card="onViewContext"
|
|
933
974
|
@item-drag-start="onRowDragStart"
|
|
934
975
|
@item-drop-into="onViewDropInto"
|
|
@@ -13,6 +13,7 @@ import { hasInternalDrag } from '../lib/dragOut';
|
|
|
13
13
|
import type { FileNode } from '../types/FileNode';
|
|
14
14
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
15
15
|
import { useLocale } from '../composables/useLocale';
|
|
16
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
16
17
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
17
18
|
import {
|
|
18
19
|
createFilePreviews,
|
|
@@ -76,7 +77,7 @@ const props = defineProps<{
|
|
|
76
77
|
}>();
|
|
77
78
|
|
|
78
79
|
const emit = defineEmits<{
|
|
79
|
-
(e: 'click-card', node: FileNode, mod:
|
|
80
|
+
(e: 'click-card', node: FileNode, mod: ClickMod): void;
|
|
80
81
|
(e: 'dbl-card', node: FileNode): void;
|
|
81
82
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
82
83
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -158,7 +159,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
function onClick(n: FileNode, ev: MouseEvent) {
|
|
161
|
-
emit('click-card', n,
|
|
162
|
+
emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-gal__label'));
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
function onDbl(n: FileNode) {
|
|
@@ -205,27 +206,10 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
|
205
206
|
emit('item-drop-into', n, ev);
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
pressTarget = n;
|
|
213
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
214
|
-
pressTimer = setTimeout(() => {
|
|
215
|
-
if (pressTarget) {
|
|
216
|
-
const t0 = ev.touches[0];
|
|
217
|
-
emit('context-card', pressTarget, {
|
|
218
|
-
clientX: t0.clientX,
|
|
219
|
-
clientY: t0.clientY,
|
|
220
|
-
preventDefault: () => {},
|
|
221
|
-
} as unknown as MouseEvent);
|
|
222
|
-
}
|
|
223
|
-
}, 500);
|
|
224
|
-
}
|
|
225
|
-
function cancelPress() {
|
|
226
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
227
|
-
pressTarget = null;
|
|
228
|
-
}
|
|
209
|
+
/* Long press → the card's menu; a tap is reported as a tap (issue #26). */
|
|
210
|
+
const touch = useRowTouch<FileNode>((n, at) =>
|
|
211
|
+
emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
|
|
212
|
+
);
|
|
229
213
|
|
|
230
214
|
/**
|
|
231
215
|
* zaman:z1 — routed through useLocale.formatDate, the package's one date
|
|
@@ -283,9 +267,9 @@ function metaFor(n: FileNode): string {
|
|
|
283
267
|
@dragover="onItemDragOver(n, $event)"
|
|
284
268
|
@dragleave="onItemDragLeave(n)"
|
|
285
269
|
@drop="onItemDrop(n, $event)"
|
|
286
|
-
@touchstart.passive="onTouchStart(n, $event)"
|
|
287
|
-
@touchend="
|
|
288
|
-
@touchmove="
|
|
270
|
+
@touchstart.passive="touch.onTouchStart(n, $event)"
|
|
271
|
+
@touchend.passive="touch.onTouchEnd"
|
|
272
|
+
@touchmove.passive="touch.onTouchMove"
|
|
289
273
|
>
|
|
290
274
|
<div class="fe-gal__thumb" :ref="(el) => bindPreview(el, n)">
|
|
291
275
|
<!-- gorunum:v1-preview — a text-readable file shows its own first
|
|
@@ -20,6 +20,7 @@ import type { FileNode } from '../types/FileNode';
|
|
|
20
20
|
import { hasInternalDrag } from '../lib/dragOut';
|
|
21
21
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
22
22
|
import { useLocale } from '../composables/useLocale';
|
|
23
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
23
24
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
24
25
|
import {
|
|
25
26
|
createFilePreviews,
|
|
@@ -110,7 +111,7 @@ const props = defineProps<{
|
|
|
110
111
|
}>();
|
|
111
112
|
|
|
112
113
|
const emit = defineEmits<{
|
|
113
|
-
(e: 'click-card', node: FileNode, mod:
|
|
114
|
+
(e: 'click-card', node: FileNode, mod: ClickMod): void;
|
|
114
115
|
(e: 'dbl-card', node: FileNode): void;
|
|
115
116
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
116
117
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -267,7 +268,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
function onClick(n: FileNode, ev: MouseEvent) {
|
|
270
|
-
emit('click-card', n,
|
|
271
|
+
emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-grid__label'));
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
function onDbl(n: FileNode) {
|
|
@@ -327,26 +328,10 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
|
327
328
|
emit('item-drop-into', n, ev);
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
335
|
-
pressTimer = setTimeout(() => {
|
|
336
|
-
if (pressTarget) {
|
|
337
|
-
const t0 = ev.touches[0];
|
|
338
|
-
emit('context-card', pressTarget, {
|
|
339
|
-
clientX: t0.clientX,
|
|
340
|
-
clientY: t0.clientY,
|
|
341
|
-
preventDefault: () => {},
|
|
342
|
-
} as unknown as MouseEvent);
|
|
343
|
-
}
|
|
344
|
-
}, 500);
|
|
345
|
-
}
|
|
346
|
-
function cancelPress() {
|
|
347
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
348
|
-
pressTarget = null;
|
|
349
|
-
}
|
|
331
|
+
/* Long press → the card's menu; a tap is reported as a tap (issue #26). */
|
|
332
|
+
const touch = useRowTouch<FileNode>((n, at) =>
|
|
333
|
+
emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
|
|
334
|
+
);
|
|
350
335
|
|
|
351
336
|
/**
|
|
352
337
|
* The card's second line. A file prints "1.7 MB • Sep 9, 2026"; a folder
|
|
@@ -432,9 +417,9 @@ function snippetTitle(snippet: string): string {
|
|
|
432
417
|
@dragover="onItemDragOver(n, $event)"
|
|
433
418
|
@dragleave="onItemDragLeave(n) /* wiring:c4 */"
|
|
434
419
|
@drop="onItemDrop(n, $event)"
|
|
435
|
-
@touchstart.passive="onTouchStart(n, $event)"
|
|
436
|
-
@touchend="
|
|
437
|
-
@touchmove="
|
|
420
|
+
@touchstart.passive="touch.onTouchStart(n, $event)"
|
|
421
|
+
@touchend.passive="touch.onTouchEnd"
|
|
422
|
+
@touchmove.passive="touch.onTouchMove"
|
|
438
423
|
>
|
|
439
424
|
<!-- gorunum:v1 — the preview, files only: 184×108. A thumbnail when
|
|
440
425
|
there is one, otherwise the type tile centred on --fe-bg-elev.
|
|
@@ -562,11 +547,15 @@ function snippetTitle(snippet: string): string {
|
|
|
562
547
|
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
563
548
|
</div>
|
|
564
549
|
<div class="fe-grid__meta">{{ captionFor(n) }}</div>
|
|
550
|
+
<!-- A card at a storage's root has no parent folder to name: no line,
|
|
551
|
+
not an em dash — the same rule the gallery follows and the list's
|
|
552
|
+
Location cell (tablo:t1). The dash read as a stray character under
|
|
553
|
+
the filename in search results. -->
|
|
565
554
|
<div
|
|
566
|
-
v-if="showParentPath"
|
|
555
|
+
v-if="showParentPath && parentDirOf(n.path)"
|
|
567
556
|
class="fe-grid__parent"
|
|
568
557
|
:title="parentDirOf(n.path)"
|
|
569
|
-
>{{ parentDirOf(n.path)
|
|
558
|
+
>{{ parentDirOf(n.path) }}</div>
|
|
570
559
|
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
571
560
|
<div v-if="cardSnippet(n)" class="fe-grid__snippet" :title="snippetTitle(cardSnippet(n))">
|
|
572
561
|
<template v-for="(seg, si) in snippetSegments(cardSnippet(n))" :key="si">
|
|
@@ -11,6 +11,7 @@ import { hasInternalDrag } from '../lib/dragOut';
|
|
|
11
11
|
import type { FileNode } from '../types/FileNode';
|
|
12
12
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
13
13
|
import { useLocale } from '../composables/useLocale';
|
|
14
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
14
15
|
import {
|
|
15
16
|
arrivedFromOutside,
|
|
16
17
|
ownedByViewer,
|
|
@@ -138,7 +139,7 @@ const props = defineProps<{
|
|
|
138
139
|
}>();
|
|
139
140
|
|
|
140
141
|
const emit = defineEmits<{
|
|
141
|
-
(e: 'click-row', node: FileNode, mod:
|
|
142
|
+
(e: 'click-row', node: FileNode, mod: ClickMod): void;
|
|
142
143
|
(e: 'dbl-row', node: FileNode): void;
|
|
143
144
|
(e: 'context-row', node: FileNode, ev: MouseEvent): void;
|
|
144
145
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -173,7 +174,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function onRowClick(n: FileNode, ev: MouseEvent) {
|
|
176
|
-
emit('click-row', n,
|
|
177
|
+
emit('click-row', n, clickMod(ev, touch.isTap(ev), '.fe-list__name'));
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
function onRowDbl(n: FileNode) {
|
|
@@ -264,28 +265,10 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
|
264
265
|
emit('item-drop-into', n, ev);
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
pressTarget = n;
|
|
272
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
273
|
-
pressTimer = setTimeout(() => {
|
|
274
|
-
if (pressTarget) {
|
|
275
|
-
const t0 = ev.touches[0];
|
|
276
|
-
emit('context-row', pressTarget, {
|
|
277
|
-
clientX: t0.clientX,
|
|
278
|
-
clientY: t0.clientY,
|
|
279
|
-
preventDefault: () => {},
|
|
280
|
-
} as unknown as MouseEvent);
|
|
281
|
-
}
|
|
282
|
-
}, 500);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function cancelPress() {
|
|
286
|
-
if (pressTimer) clearTimeout(pressTimer);
|
|
287
|
-
pressTarget = null;
|
|
288
|
-
}
|
|
268
|
+
/* Long press → the row's menu; a tap is reported as a tap (issue #26). */
|
|
269
|
+
const touch = useRowTouch<FileNode>((n, at) =>
|
|
270
|
+
emit('context-row', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
|
|
271
|
+
);
|
|
289
272
|
|
|
290
273
|
function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
291
274
|
if (b === 'kept') return '\u2713';
|
|
@@ -359,7 +342,12 @@ function thumbOf(n: FileNode): string | null {
|
|
|
359
342
|
return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
|
|
360
343
|
}
|
|
361
344
|
|
|
362
|
-
/**
|
|
345
|
+
/** Whether the Type column is on screen. While it is, it already says "Folder"
|
|
346
|
+
* in its own cell, and the word beside the name printed it twice per row. */
|
|
347
|
+
const typeColumnShown = computed(() => visibleCols.value.includes('type'));
|
|
348
|
+
|
|
349
|
+
/** The muted "Folder" said beside a directory's name — not on the sentinels,
|
|
350
|
+
* and only while no Type column says it (see typeColumnShown). */
|
|
363
351
|
function isPlainDir(n: FileNode): boolean {
|
|
364
352
|
return n.type === 'dir' && !isPinnedSpecial(n);
|
|
365
353
|
}
|
|
@@ -1237,9 +1225,9 @@ const segments = computed<DateRun<FileNode>[]>(() =>
|
|
|
1237
1225
|
@dragover="onItemDragOver(n, $event)"
|
|
1238
1226
|
@dragleave="onItemDragLeave(n) /* wiring:c4 */"
|
|
1239
1227
|
@drop="onItemDrop(n, $event)"
|
|
1240
|
-
@touchstart.passive="onTouchStart(n, $event)"
|
|
1241
|
-
@touchend="
|
|
1242
|
-
@touchmove="
|
|
1228
|
+
@touchstart.passive="touch.onTouchStart(n, $event)"
|
|
1229
|
+
@touchend.passive="touch.onTouchEnd"
|
|
1230
|
+
@touchmove.passive="touch.onTouchMove"
|
|
1243
1231
|
>
|
|
1244
1232
|
<!-- gorunum:v1 — the tick drives the SAME selection the row click
|
|
1245
1233
|
does (onCheckClick → click-row → useSelection.click), so shift
|
|
@@ -1324,7 +1312,7 @@ const segments = computed<DateRun<FileNode>[]>(() =>
|
|
|
1324
1312
|
`.fe-list__name`: the shot scripts read that element's
|
|
1325
1313
|
textContent as the filename (e2e/shots/starstags.mjs), and a
|
|
1326
1314
|
row whose name reads "Reports Folder" breaks them silently. -->
|
|
1327
|
-
<span v-if="isPlainDir(n)" class="fe-list__kind">{{ t('node.folder') }}</span>
|
|
1315
|
+
<span v-if="isPlainDir(n) && !typeColumnShown" class="fe-list__kind">{{ t('node.folder') }}</span>
|
|
1328
1316
|
</div>
|
|
1329
1317
|
<!-- tablo:t1 — one loop over the SAME list the header draws, so a
|
|
1330
1318
|
column cannot exist in one and not the other, and the values land
|
|
@@ -17,6 +17,7 @@ import { watch } from 'vue';
|
|
|
17
17
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
18
18
|
import type { PendingOp } from '../composables/usePendingOps';
|
|
19
19
|
import type { OperationsStore, OperationStatus } from '../composables/useOperations';
|
|
20
|
+
import { opPercent } from '../lib/opProgress';
|
|
20
21
|
|
|
21
22
|
const props = defineProps<{
|
|
22
23
|
ops: PendingOp[];
|
|
@@ -35,11 +36,9 @@ function mapStatus(op: PendingOp): OperationStatus {
|
|
|
35
36
|
return 'running'; // pending | running
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return Math.min(100, Math.round((op.progress_done / op.progress_total) * 100));
|
|
42
|
-
}
|
|
39
|
+
/* issue #27 — bytes when a cross-storage transfer reports them, no fake 0%
|
|
40
|
+
* otherwise; the rule is shared with the admin tray (lib/opProgress). */
|
|
41
|
+
const percentOf = (op: PendingOp): number | null => opPercent(op);
|
|
43
42
|
|
|
44
43
|
watch(
|
|
45
44
|
() => props.ops,
|
|
@@ -17,15 +17,16 @@
|
|
|
17
17
|
* device's — by asking the resolver with the viewer tier blanked. A reset
|
|
18
18
|
* whose outcome you have to guess is not a reset.
|
|
19
19
|
*/
|
|
20
|
-
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
20
|
+
import { computed, inject, onBeforeUnmount, ref, watch } from 'vue';
|
|
21
21
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
22
22
|
import { formatInstant, useLocale } from '../composables/useLocale';
|
|
23
23
|
import {
|
|
24
|
+
EXPLORER_CLOCK,
|
|
24
25
|
deviceTimeZone,
|
|
25
26
|
resolveTimeZone,
|
|
26
|
-
|
|
27
|
+
resolvedTimeZoneFor,
|
|
27
28
|
setViewerTimeZone,
|
|
28
|
-
|
|
29
|
+
timeZoneSourcesFor,
|
|
29
30
|
viewerTimeZone,
|
|
30
31
|
} from '../lib/timezone';
|
|
31
32
|
import Modal from '../modals/Modal.vue';
|
|
@@ -43,8 +44,10 @@ const { t } = useLocale(() => props.locale);
|
|
|
43
44
|
|
|
44
45
|
const inputId = `fe-tzdialog-${Math.random().toString(36).slice(2, 9)}`;
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
const
|
|
47
|
+
// This explorer's clock, not the page's newest (lib/timezone, timeZoneSourcesFor).
|
|
48
|
+
const clock = inject(EXPLORER_CLOCK, undefined);
|
|
49
|
+
const fallback = computed(() => resolveTimeZone({ ...timeZoneSourcesFor(clock), viewer: '' }));
|
|
50
|
+
const inForce = computed(() => resolvedTimeZoneFor(clock));
|
|
48
51
|
const inForceZone = computed(() => inForce.value.zone ?? deviceTimeZone());
|
|
49
52
|
|
|
50
53
|
/** "Right now in Asia/Tokyo: 14:05:09" — the fastest way to see a wrong pick. */
|
|
@@ -54,11 +57,16 @@ function refresh(): void {
|
|
|
54
57
|
// Through the shared formatter, which reads the RESOLVED zone — the readout
|
|
55
58
|
// must say what every date on the page is actually drawn in, not what this
|
|
56
59
|
// dialog thinks it chose.
|
|
57
|
-
now.value = formatInstant(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
now.value = formatInstant(
|
|
61
|
+
new Date(),
|
|
62
|
+
props.locale,
|
|
63
|
+
{
|
|
64
|
+
hour: '2-digit',
|
|
65
|
+
minute: '2-digit',
|
|
66
|
+
second: '2-digit',
|
|
67
|
+
},
|
|
68
|
+
clock,
|
|
69
|
+
);
|
|
62
70
|
}
|
|
63
71
|
watch(
|
|
64
72
|
() => props.open,
|
|
@@ -101,19 +101,28 @@ onMounted(async () => {
|
|
|
101
101
|
emit('active', { hasToken: tokens.value.length > 0 });
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The name a token gets when the field is left empty — one the user will
|
|
106
|
+
* recognise in the list later.
|
|
107
|
+
*
|
|
108
|
+
* A protocol guide knows both halves: `WebDAV — files.example.com`, the host
|
|
109
|
+
* being the PUBLIC address the guide resolved (connectionsOrigin). The API
|
|
110
|
+
* keys page knows neither, and it used to make a host up from `apiBase`,
|
|
111
|
+
* which is whatever the page happens to talk to: every key on a local
|
|
112
|
+
* instance was called `127.0.0.1:5297`, and that address was printed as the
|
|
113
|
+
* placeholder in the README screenshots. It names the key by the day it was
|
|
114
|
+
* made instead.
|
|
115
|
+
*/
|
|
104
116
|
function defaultLabel(): string {
|
|
105
117
|
const p = (props.protocol || '').toUpperCase();
|
|
106
|
-
|
|
118
|
+
if (p) return props.host ? `${p} — ${props.host}` : p;
|
|
119
|
+
return t('conn.tokens.defaultName', { date: new Date().toISOString().slice(0, 10) });
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return new URL(props.config.apiBase || window.location.origin).host;
|
|
114
|
-
} catch {
|
|
115
|
-
return 'filex';
|
|
116
|
-
}
|
|
122
|
+
/** What the empty name field suggests: the saved default on a protocol guide,
|
|
123
|
+
* an example of a useful name on the API keys page. */
|
|
124
|
+
function labelPlaceholder(): string {
|
|
125
|
+
return props.protocol ? defaultLabel() : t('conn.tokens.namePlaceholder');
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
async function mint(): Promise<void> {
|
|
@@ -206,7 +215,7 @@ function usedLabel(row: ApiToken): string {
|
|
|
206
215
|
<input
|
|
207
216
|
v-model="label"
|
|
208
217
|
class="fe-cfield__input"
|
|
209
|
-
:placeholder="
|
|
218
|
+
:placeholder="labelPlaceholder()"
|
|
210
219
|
data-testid="token-label"
|
|
211
220
|
/>
|
|
212
221
|
<button class="fe-s3keys__btn" :disabled="busy" data-testid="token-mint" @click="mint">
|
|
@@ -220,7 +229,7 @@ function usedLabel(row: ApiToken): string {
|
|
|
220
229
|
<input
|
|
221
230
|
v-model="label"
|
|
222
231
|
class="fe-cfield__input"
|
|
223
|
-
:placeholder="
|
|
232
|
+
:placeholder="labelPlaceholder()"
|
|
224
233
|
data-testid="token-label"
|
|
225
234
|
/>
|
|
226
235
|
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
* the browser's zone answers. A clock must never throw.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { onBeforeUnmount, watch, type Ref } from 'vue';
|
|
35
|
+
import { onBeforeUnmount, provide, watch, type Ref } from 'vue';
|
|
36
36
|
import type { ExplorerConfig } from '../types/ExplorerConfig';
|
|
37
37
|
import type { Capabilities } from '../types/FileNode';
|
|
38
|
-
import { releaseTimeZoneOwner, setAccountTimeZone, setHostTimeZone } from '../lib/timezone';
|
|
38
|
+
import { EXPLORER_CLOCK, releaseTimeZoneOwner, setAccountTimeZone, setHostTimeZone } from '../lib/timezone';
|
|
39
39
|
|
|
40
40
|
export interface ExplorerTimeZoneDeps {
|
|
41
41
|
config: () => ExplorerConfig;
|
|
@@ -45,8 +45,11 @@ export interface ExplorerTimeZoneDeps {
|
|
|
45
45
|
fetchMe: () => Promise<{ user?: { timezone?: unknown } | null } | undefined>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function useExplorerTimeZone(deps: ExplorerTimeZoneDeps):
|
|
48
|
+
export function useExplorerTimeZone(deps: ExplorerTimeZoneDeps): symbol {
|
|
49
49
|
const owner = Symbol('filex-explorer');
|
|
50
|
+
// Every component under this explorer reads its dates on this owner's
|
|
51
|
+
// tiers, not the page's newest (lib/timezone, timeZoneSourcesFor).
|
|
52
|
+
provide(EXPLORER_CLOCK, owner);
|
|
50
53
|
|
|
51
54
|
watch(
|
|
52
55
|
() => deps.config().timeZone,
|
|
@@ -82,4 +85,5 @@ export function useExplorerTimeZone(deps: ExplorerTimeZoneDeps): void {
|
|
|
82
85
|
);
|
|
83
86
|
|
|
84
87
|
onBeforeUnmount(() => releaseTimeZoneOwner(owner));
|
|
88
|
+
return owner;
|
|
85
89
|
}
|