@brftech/filex-core 0.41.1 → 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-DxxjhYZD.js → ArchiveViewer-Dq1c4-G5.js} +2 -2
- package/dist/{ArchiveViewer-DxxjhYZD.js.map → ArchiveViewer-Dq1c4-G5.js.map} +1 -1
- package/dist/{CsvViewer-CyuwENI_.js → CsvViewer-CeHslAc0.js} +2 -2
- package/dist/{CsvViewer-CyuwENI_.js.map → CsvViewer-CeHslAc0.js.map} +1 -1
- package/dist/{DrawioViewer-BIYsTX97.js → DrawioViewer-p03u7Oda.js} +2 -2
- package/dist/{DrawioViewer-BIYsTX97.js.map → DrawioViewer-p03u7Oda.js.map} +1 -1
- package/dist/{EpubViewer-K9PjCMmS.js → EpubViewer-D08Y-Tww.js} +2 -2
- package/dist/{EpubViewer-K9PjCMmS.js.map → EpubViewer-D08Y-Tww.js.map} +1 -1
- package/dist/{IpynbViewer-DYU6Zgz2.js → IpynbViewer-mG7p3Sil.js} +2 -2
- package/dist/{IpynbViewer-DYU6Zgz2.js.map → IpynbViewer-mG7p3Sil.js.map} +1 -1
- package/dist/{MermaidViewer-Bp73aId8.js → MermaidViewer-CCPg1Q3W.js} +2 -2
- package/dist/{MermaidViewer-Bp73aId8.js.map → MermaidViewer-CCPg1Q3W.js.map} +1 -1
- package/dist/{PsdViewer-C2eLivw1.js → PsdViewer-CDEHTiUN.js} +2 -2
- package/dist/{PsdViewer-C2eLivw1.js.map → PsdViewer-CDEHTiUN.js.map} +1 -1
- package/dist/{TiffViewer-DsHg5gcz.js → TiffViewer-DnWRUGOK.js} +2 -2
- package/dist/{TiffViewer-DsHg5gcz.js.map → TiffViewer-DnWRUGOK.js.map} +1 -1
- package/dist/{Viewer3D-C_dc2HN9.js → Viewer3D-COCQp53D.js} +2 -2
- package/dist/{Viewer3D-C_dc2HN9.js.map → Viewer3D-COCQp53D.js.map} +1 -1
- package/dist/filex-core.js +81 -80
- package/dist/filex-core.umd.cjs +43 -43
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-vxMZpYb-.js → index-JUiZzLmF.js} +6685 -6653
- package/dist/index-JUiZzLmF.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/package.json +1 -1
- package/src/components/FilePane.vue +38 -9
- package/src/components/GalleryView.vue +3 -3
- package/src/components/GridView.vue +9 -5
- package/src/components/ListView.vue +3 -3
- package/src/components/PendingOpsTray.vue +4 -5
- package/src/composables/usePendingOps.ts +5 -0
- package/src/composables/useRowTouch.ts +31 -0
- package/src/index.ts +5 -0
- package/src/lib/opProgress.ts +43 -0
- package/dist/index-vxMZpYb-.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -5263,6 +5263,37 @@ export declare type OperationsStore = ReturnType<typeof useOperations>;
|
|
|
5263
5263
|
|
|
5264
5264
|
export declare type OperationStatus = 'running' | 'done' | 'error' | 'aborted';
|
|
5265
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
|
+
|
|
5266
5297
|
declare type PaneId = 'main' | 'split';
|
|
5267
5298
|
|
|
5268
5299
|
/**
|
|
@@ -5294,6 +5325,9 @@ export declare interface PendingOp {
|
|
|
5294
5325
|
status: 'pending' | 'running' | 'done' | 'error';
|
|
5295
5326
|
progress_total: number;
|
|
5296
5327
|
progress_done: number;
|
|
5328
|
+
/** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
|
|
5329
|
+
bytes_total?: number;
|
|
5330
|
+
bytes_done?: number;
|
|
5297
5331
|
target_path: string | null;
|
|
5298
5332
|
source_dir: string | null;
|
|
5299
5333
|
source_count: number;
|
|
@@ -7012,6 +7046,8 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
|
|
|
7012
7046
|
status: "pending" | "running" | "done" | "error";
|
|
7013
7047
|
progress_total: number;
|
|
7014
7048
|
progress_done: number;
|
|
7049
|
+
bytes_total?: number | undefined;
|
|
7050
|
+
bytes_done?: number | undefined;
|
|
7015
7051
|
target_path: string | null;
|
|
7016
7052
|
source_dir: string | null;
|
|
7017
7053
|
source_count: number;
|
|
@@ -7025,6 +7061,8 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
|
|
|
7025
7061
|
status: "pending" | "running" | "done" | "error";
|
|
7026
7062
|
progress_total: number;
|
|
7027
7063
|
progress_done: number;
|
|
7064
|
+
bytes_total?: number | undefined;
|
|
7065
|
+
bytes_done?: number | undefined;
|
|
7028
7066
|
target_path: string | null;
|
|
7029
7067
|
source_dir: string | null;
|
|
7030
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';
|
|
@@ -443,12 +444,35 @@ 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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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. */
|
|
452
476
|
if (mod.touch && !mod.ctrl && !mod.shift) {
|
|
453
477
|
if (props.selected.size === 0) {
|
|
454
478
|
onRowOpen(n);
|
|
@@ -460,6 +484,11 @@ function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean; touch?:
|
|
|
460
484
|
emit('click-row', n, mod);
|
|
461
485
|
}
|
|
462
486
|
|
|
487
|
+
function onViewDbl(n: FileNode) {
|
|
488
|
+
if (withinNameOpenGuard()) return;
|
|
489
|
+
onRowOpen(n);
|
|
490
|
+
}
|
|
491
|
+
|
|
463
492
|
function onViewContext(n: FileNode, ev: MouseEvent) {
|
|
464
493
|
ev.preventDefault();
|
|
465
494
|
ev.stopPropagation();
|
|
@@ -891,7 +920,7 @@ watch(panePath, () => {
|
|
|
891
920
|
:auth-credentials="authCredentials"
|
|
892
921
|
@click-row="onViewClick"
|
|
893
922
|
@display-order="(nodes: FileNode[]) => emit('display-order', nodes)"
|
|
894
|
-
@dbl-row="
|
|
923
|
+
@dbl-row="onViewDbl"
|
|
895
924
|
@context-row="onViewContext"
|
|
896
925
|
@item-drag-start="onRowDragStart"
|
|
897
926
|
@item-drop-into="onViewDropInto"
|
|
@@ -917,7 +946,7 @@ watch(panePath, () => {
|
|
|
917
946
|
:auth-credentials="authCredentials"
|
|
918
947
|
@click-card="onViewClick"
|
|
919
948
|
@display-order="(nodes: FileNode[]) => emit('display-order', nodes)"
|
|
920
|
-
@dbl-card="
|
|
949
|
+
@dbl-card="onViewDbl"
|
|
921
950
|
@context-card="onViewContext"
|
|
922
951
|
@item-drag-start="onRowDragStart"
|
|
923
952
|
@item-drop-into="onViewDropInto"
|
|
@@ -940,7 +969,7 @@ watch(panePath, () => {
|
|
|
940
969
|
:auth-headers="authHeaders"
|
|
941
970
|
:auth-credentials="authCredentials"
|
|
942
971
|
@click-card="onViewClick"
|
|
943
|
-
@dbl-card="
|
|
972
|
+
@dbl-card="onViewDbl"
|
|
944
973
|
@context-card="onViewContext"
|
|
945
974
|
@item-drag-start="onRowDragStart"
|
|
946
975
|
@item-drop-into="onViewDropInto"
|
|
@@ -13,7 +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 { useRowTouch } from '../composables/useRowTouch';
|
|
16
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
17
17
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
18
18
|
import {
|
|
19
19
|
createFilePreviews,
|
|
@@ -77,7 +77,7 @@ const props = defineProps<{
|
|
|
77
77
|
}>();
|
|
78
78
|
|
|
79
79
|
const emit = defineEmits<{
|
|
80
|
-
(e: 'click-card', node: FileNode, mod:
|
|
80
|
+
(e: 'click-card', node: FileNode, mod: ClickMod): void;
|
|
81
81
|
(e: 'dbl-card', node: FileNode): void;
|
|
82
82
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
83
83
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -159,7 +159,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function onClick(n: FileNode, ev: MouseEvent) {
|
|
162
|
-
emit('click-card', n,
|
|
162
|
+
emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-gal__label'));
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function onDbl(n: FileNode) {
|
|
@@ -20,7 +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 { useRowTouch } from '../composables/useRowTouch';
|
|
23
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
24
24
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
25
25
|
import {
|
|
26
26
|
createFilePreviews,
|
|
@@ -111,7 +111,7 @@ const props = defineProps<{
|
|
|
111
111
|
}>();
|
|
112
112
|
|
|
113
113
|
const emit = defineEmits<{
|
|
114
|
-
(e: 'click-card', node: FileNode, mod:
|
|
114
|
+
(e: 'click-card', node: FileNode, mod: ClickMod): void;
|
|
115
115
|
(e: 'dbl-card', node: FileNode): void;
|
|
116
116
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
117
117
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -268,7 +268,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
function onClick(n: FileNode, ev: MouseEvent) {
|
|
271
|
-
emit('click-card', n,
|
|
271
|
+
emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-grid__label'));
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
function onDbl(n: FileNode) {
|
|
@@ -547,11 +547,15 @@ function snippetTitle(snippet: string): string {
|
|
|
547
547
|
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
548
548
|
</div>
|
|
549
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. -->
|
|
550
554
|
<div
|
|
551
|
-
v-if="showParentPath"
|
|
555
|
+
v-if="showParentPath && parentDirOf(n.path)"
|
|
552
556
|
class="fe-grid__parent"
|
|
553
557
|
:title="parentDirOf(n.path)"
|
|
554
|
-
>{{ parentDirOf(n.path)
|
|
558
|
+
>{{ parentDirOf(n.path) }}</div>
|
|
555
559
|
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
556
560
|
<div v-if="cardSnippet(n)" class="fe-grid__snippet" :title="snippetTitle(cardSnippet(n))">
|
|
557
561
|
<template v-for="(seg, si) in snippetSegments(cardSnippet(n))" :key="si">
|
|
@@ -11,7 +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 { useRowTouch } from '../composables/useRowTouch';
|
|
14
|
+
import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
15
15
|
import {
|
|
16
16
|
arrivedFromOutside,
|
|
17
17
|
ownedByViewer,
|
|
@@ -139,7 +139,7 @@ const props = defineProps<{
|
|
|
139
139
|
}>();
|
|
140
140
|
|
|
141
141
|
const emit = defineEmits<{
|
|
142
|
-
(e: 'click-row', node: FileNode, mod:
|
|
142
|
+
(e: 'click-row', node: FileNode, mod: ClickMod): void;
|
|
143
143
|
(e: 'dbl-row', node: FileNode): void;
|
|
144
144
|
(e: 'context-row', node: FileNode, ev: MouseEvent): void;
|
|
145
145
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
@@ -174,7 +174,7 @@ function isSelected(n: FileNode): boolean {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
function onRowClick(n: FileNode, ev: MouseEvent) {
|
|
177
|
-
emit('click-row', n,
|
|
177
|
+
emit('click-row', n, clickMod(ev, touch.isTap(ev), '.fe-list__name'));
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function onRowDbl(n: FileNode) {
|
|
@@ -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,
|
|
@@ -23,6 +23,9 @@ export interface PendingOp {
|
|
|
23
23
|
status: 'pending' | 'running' | 'done' | 'error';
|
|
24
24
|
progress_total: number;
|
|
25
25
|
progress_done: number;
|
|
26
|
+
/** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
|
|
27
|
+
bytes_total?: number;
|
|
28
|
+
bytes_done?: number;
|
|
26
29
|
target_path: string | null;
|
|
27
30
|
source_dir: string | null;
|
|
28
31
|
source_count: number;
|
|
@@ -69,6 +72,8 @@ function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
|
69
72
|
status,
|
|
70
73
|
progress_total: num(raw.progress_total, raw.total, sources.length),
|
|
71
74
|
progress_done: num(raw.progress_done, raw.done),
|
|
75
|
+
bytes_total: num(raw.bytes_total),
|
|
76
|
+
bytes_done: num(raw.bytes_done),
|
|
72
77
|
target_path: str(raw.target_path, raw.dest),
|
|
73
78
|
source_dir: str(raw.source_dir),
|
|
74
79
|
source_count: num(raw.source_count, sources.length),
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The views only report. What a tap means is decided once, in FilePane.
|
|
15
15
|
*
|
|
16
|
+
* Issue #26, second round (the reporter's rule, on EVERY device):
|
|
17
|
+
* - a press on the item's NAME opens it — mouse click or finger tap, with or
|
|
18
|
+
* without a selection;
|
|
19
|
+
* - a long press (finger) or a right click (mouse) opens the menu;
|
|
20
|
+
* - the checkbox selects, exactly as before.
|
|
21
|
+
* So the views also report whether the click landed on the name (`name`).
|
|
22
|
+
*
|
|
16
23
|
* ⚠ A tap is judged from the gesture that produced the click, never from the
|
|
17
24
|
* screen: `pointerType` where the browser sets it on click (Chromium, Firefox),
|
|
18
25
|
* and the touchend that just preceded the click where it does not (older
|
|
@@ -37,6 +44,30 @@ export interface TouchPoint {
|
|
|
37
44
|
clientY: number;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/** What a view reports about a click on one of its items. */
|
|
48
|
+
export interface ClickMod {
|
|
49
|
+
ctrl: boolean;
|
|
50
|
+
shift: boolean;
|
|
51
|
+
/** The click was a finger's tap. */
|
|
52
|
+
touch?: boolean;
|
|
53
|
+
/** The click landed on the item's name. */
|
|
54
|
+
name?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The one place a view turns a click into a `ClickMod`. `nameSelector` is the
|
|
59
|
+
* view's own name element (`.fe-list__name`, `.fe-grid__label`, …).
|
|
60
|
+
*/
|
|
61
|
+
export function clickMod(ev: MouseEvent, touch: boolean, nameSelector: string): ClickMod {
|
|
62
|
+
const target = ev.target as Element | null;
|
|
63
|
+
return {
|
|
64
|
+
ctrl: ev.ctrlKey || ev.metaKey,
|
|
65
|
+
shift: ev.shiftKey,
|
|
66
|
+
touch,
|
|
67
|
+
name: typeof target?.closest === 'function' && target.closest(nameSelector) !== null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
|
|
41
72
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
42
73
|
let target: T | null = null;
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,11 @@ export {
|
|
|
55
55
|
} from './lib/destinationTree';
|
|
56
56
|
export type { DestinationRow } from './lib/destinationTree';
|
|
57
57
|
|
|
58
|
+
/* issue #27 — the one "how far along is this queued op" rule, shared by the
|
|
59
|
+
* explorer's operations center and a host's own tray (the admin app's). */
|
|
60
|
+
export { opPercent } from './lib/opProgress';
|
|
61
|
+
export type { OpProgressLike } from './lib/opProgress';
|
|
62
|
+
|
|
58
63
|
/* tasi:m1 — "download the selection as one archive". Exported so a host that
|
|
59
64
|
* draws its own selection bar gets the real two-step flow (authorized mint,
|
|
60
65
|
* then a navigation that streams) instead of reaching for window.open per
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opProgress — the one rule for "how far along is this queued operation".
|
|
3
|
+
*
|
|
4
|
+
* Both progress surfaces read it: the explorer's operations center
|
|
5
|
+
* (PendingOpsTray → OperationsCenter) and the admin app's bottom-right tray.
|
|
6
|
+
* Two copies of this rule are how one tray starts saying something the other
|
|
7
|
+
* does not (filex lesson #119).
|
|
8
|
+
*
|
|
9
|
+
* Issue #27: a queued op counts SOURCES, so moving one large file — or one
|
|
10
|
+
* folder of a thousand files — is "0 of 1" until the very end. Drawn as a
|
|
11
|
+
* percentage that is a bar frozen at 0% that then vanishes, which the reporter
|
|
12
|
+
* read as stuck. So:
|
|
13
|
+
*
|
|
14
|
+
* - a running cross-storage transfer reports bytes (`bytes_done` /
|
|
15
|
+
* `bytes_total`); when the total is known that is the percentage;
|
|
16
|
+
* - bytes moving with no total yet, or a single source with nothing but a
|
|
17
|
+
* source count, has NO honest percentage: `null`, and the surface shows
|
|
18
|
+
* an indeterminate indicator instead of a fake 0%;
|
|
19
|
+
* - several sources still report per source, as before.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface OpProgressLike {
|
|
23
|
+
status: string;
|
|
24
|
+
progress_total: number;
|
|
25
|
+
progress_done: number;
|
|
26
|
+
bytes_total?: number;
|
|
27
|
+
bytes_done?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clampPercent(n: number): number {
|
|
31
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 0–100, or `null` when there is no honest number to draw. */
|
|
35
|
+
export function opPercent(op: OpProgressLike): number | null {
|
|
36
|
+
if (op.status === 'done') return 100;
|
|
37
|
+
const bytesTotal = op.bytes_total ?? 0;
|
|
38
|
+
const bytesDone = op.bytes_done ?? 0;
|
|
39
|
+
if (bytesTotal > 0) return clampPercent((bytesDone / bytesTotal) * 100);
|
|
40
|
+
if (bytesDone > 0) return null;
|
|
41
|
+
if (op.progress_total <= 1) return null;
|
|
42
|
+
return clampPercent((op.progress_done / op.progress_total) * 100);
|
|
43
|
+
}
|