@brftech/filex-core 0.1.56 → 0.1.61
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/LICENSE +21 -21
- package/README.md +108 -108
- package/dist/{ArchiveViewer-CJdaw5x3.js → ArchiveViewer-CbUUEREI.js} +20 -20
- package/dist/ArchiveViewer-CbUUEREI.js.map +1 -0
- package/dist/{CsvViewer-QYx_tePQ.js → CsvViewer-72rDsQJX.js} +3 -3
- package/dist/CsvViewer-72rDsQJX.js.map +1 -0
- package/dist/{DrawioViewer-GWt0TyB7.js → DrawioViewer-DSyMwGvg.js} +7 -7
- package/dist/DrawioViewer-DSyMwGvg.js.map +1 -0
- package/dist/{EpubViewer-Je0ywklQ.js → EpubViewer-DvDa17d7.js} +3 -3
- package/dist/EpubViewer-DvDa17d7.js.map +1 -0
- package/dist/{IpynbViewer-TsB4gqP-.js → IpynbViewer-DibIkdsz.js} +3 -3
- package/dist/IpynbViewer-DibIkdsz.js.map +1 -0
- package/dist/{MermaidViewer-CXfeDS-7.js → MermaidViewer-BoFLtGvl.js} +3 -3
- package/dist/MermaidViewer-BoFLtGvl.js.map +1 -0
- package/dist/{PsdViewer-VtPpxKVe.js → PsdViewer-K2iZkwax.js} +3 -3
- package/dist/PsdViewer-K2iZkwax.js.map +1 -0
- package/dist/{TiffViewer-OINn9bbb.js → TiffViewer-JzDzbqpO.js} +3 -3
- package/dist/TiffViewer-JzDzbqpO.js.map +1 -0
- package/dist/{Viewer3D-xvvyiWHd.js → Viewer3D-WEMYCP8l.js} +7 -7
- package/dist/Viewer3D-WEMYCP8l.js.map +1 -0
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +4 -4
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-CmSYzt6L.js → index-B5YT-m5Q.js} +209 -209
- package/dist/index-B5YT-m5Q.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumb.vue +282 -282
- package/src/components/ContextMenu.vue +152 -152
- package/src/components/GridView.vue +166 -166
- package/src/components/ListView.vue +212 -212
- package/src/components/PendingOpsTray.vue +91 -91
- package/src/components/RecentlyOpened.vue +155 -155
- package/src/components/StarButton.vue +101 -101
- package/src/components/TagPicker.vue +184 -184
- package/src/components/Toolbar.vue +220 -220
- package/src/components/UploadProgress.vue +78 -78
- package/src/composables/useFileApi.ts +692 -692
- package/src/composables/useKeyboardShortcuts.ts +144 -144
- package/src/composables/useLocale.ts +60 -60
- package/src/composables/useMonacoLoader.ts +98 -98
- package/src/composables/usePendingOps.ts +200 -200
- package/src/composables/useSelection.ts +81 -81
- package/src/composables/useUploadChunked.ts +250 -250
- package/src/composables/useViewerFetch.ts +58 -58
- package/src/index.ts +80 -80
- package/src/locales/en.ts +122 -122
- package/src/locales/index.ts +12 -12
- package/src/locales/tr.ts +122 -122
- package/src/modals/DeleteConfirmModal.vue +32 -32
- package/src/modals/Modal.vue +104 -104
- package/src/modals/NewFolderModal.vue +64 -64
- package/src/modals/PermissionsModal.vue +815 -815
- package/src/modals/PreviewModal.vue +1070 -1070
- package/src/modals/RenameModal.vue +58 -58
- package/src/modals/ShareModal.vue +135 -135
- package/src/styles/base.css +1224 -1224
- package/src/styles/variables.css +90 -90
- package/src/types/FileNode.ts +136 -136
- package/src/types/index.ts +27 -27
- package/src/types/peers.d.ts +45 -45
- package/src/viewers/ArchiveViewer.vue +186 -186
- package/src/viewers/CsvViewer.vue +319 -319
- package/src/viewers/DrawioViewer.vue +243 -243
- package/src/viewers/EpubViewer.vue +307 -307
- package/src/viewers/IpynbViewer.vue +365 -365
- package/src/viewers/MermaidViewer.vue +259 -259
- package/src/viewers/PdfViewer.vue +562 -562
- package/src/viewers/PsdViewer.vue +264 -264
- package/src/viewers/TiffViewer.vue +273 -273
- package/src/viewers/Viewer3D.vue +120 -120
- package/dist/ArchiveViewer-CJdaw5x3.js.map +0 -1
- package/dist/CsvViewer-QYx_tePQ.js.map +0 -1
- package/dist/DrawioViewer-GWt0TyB7.js.map +0 -1
- package/dist/EpubViewer-Je0ywklQ.js.map +0 -1
- package/dist/IpynbViewer-TsB4gqP-.js.map +0 -1
- package/dist/MermaidViewer-CXfeDS-7.js.map +0 -1
- package/dist/PsdViewer-VtPpxKVe.js.map +0 -1
- package/dist/TiffViewer-OINn9bbb.js.map +0 -1
- package/dist/Viewer3D-xvvyiWHd.js.map +0 -1
- package/dist/index-CmSYzt6L.js.map +0 -1
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* usePendingOps — track async copy/move/delete operations.
|
|
3
|
-
*
|
|
4
|
-
* Backend POST /files/{copy,move,delete} returns `{op}` immediately
|
|
5
|
-
* (status='pending') and the actual S3 work runs in a worker. The
|
|
6
|
-
* client polls `/files/ops` every 2s while there's anything still
|
|
7
|
-
* pending or running so the user gets live feedback ("3/5 kopyalandı").
|
|
8
|
-
*
|
|
9
|
-
* Polling is reference-counted: callers `register(op)` after starting a
|
|
10
|
-
* new op; an interval ticks while the registered set has any non-
|
|
11
|
-
* terminal entries. Terminal ops (done/error) stay in the local cache
|
|
12
|
-
* for `RETAIN_MS` so the UI can flash a final state before they fade
|
|
13
|
-
* out, then a sweep removes them.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { computed, ref, onBeforeUnmount } from 'vue';
|
|
17
|
-
import type { ExplorerConfig } from '../types/ExplorerConfig';
|
|
18
|
-
import type { FileApi } from './useFileApi';
|
|
19
|
-
|
|
20
|
-
export interface PendingOp {
|
|
21
|
-
id: number;
|
|
22
|
-
op_type: 'copy' | 'move' | 'delete';
|
|
23
|
-
status: 'pending' | 'running' | 'done' | 'error';
|
|
24
|
-
progress_total: number;
|
|
25
|
-
progress_done: number;
|
|
26
|
-
target_path: string | null;
|
|
27
|
-
source_dir: string | null;
|
|
28
|
-
source_count: number;
|
|
29
|
-
error_message: string | null;
|
|
30
|
-
started_at: string | null;
|
|
31
|
-
finished_at: string | null;
|
|
32
|
-
created_at: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const POLL_MS = 2000;
|
|
36
|
-
const RETAIN_MS = 8000;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* normalizeOp maps the backend ops row (`{kind,total,done,failed,error,dest,
|
|
40
|
-
* status:'ok'|'failed'|'partial'|…}`) onto the PendingOp contract the tray
|
|
41
|
-
* renders (`{op_type,progress_total,progress_done,error_message,status:'done'|
|
|
42
|
-
* 'error'|…}`). Without this the tray showed "undefined/undefined" for a
|
|
43
|
-
* running op (progress_* were undefined) and never treated an 'ok'/'failed'
|
|
44
|
-
* row as terminal. Tolerant of an already-normalized shape (idempotent).
|
|
45
|
-
*/
|
|
46
|
-
function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
47
|
-
const num = (...vals: unknown[]): number => {
|
|
48
|
-
for (const v of vals) if (typeof v === 'number') return v;
|
|
49
|
-
return 0;
|
|
50
|
-
};
|
|
51
|
-
const str = (...vals: unknown[]): string | null => {
|
|
52
|
-
for (const v of vals) if (typeof v === 'string' && v !== '') return v;
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
55
|
-
const rawStatus = String(raw.status ?? 'pending');
|
|
56
|
-
const status: PendingOp['status'] =
|
|
57
|
-
rawStatus === 'ok' || rawStatus === 'done'
|
|
58
|
-
? 'done'
|
|
59
|
-
: rawStatus === 'failed' || rawStatus === 'partial' || rawStatus === 'error'
|
|
60
|
-
? 'error'
|
|
61
|
-
: rawStatus === 'running'
|
|
62
|
-
? 'running'
|
|
63
|
-
: 'pending';
|
|
64
|
-
const sources = Array.isArray(raw.sources) ? raw.sources : [];
|
|
65
|
-
const opType = str(raw.op_type, raw.kind) ?? 'delete';
|
|
66
|
-
return {
|
|
67
|
-
id: num(raw.id),
|
|
68
|
-
op_type: (opType === 'copy' || opType === 'move' ? opType : 'delete') as PendingOp['op_type'],
|
|
69
|
-
status,
|
|
70
|
-
progress_total: num(raw.progress_total, raw.total, sources.length),
|
|
71
|
-
progress_done: num(raw.progress_done, raw.done),
|
|
72
|
-
target_path: str(raw.target_path, raw.dest),
|
|
73
|
-
source_dir: str(raw.source_dir),
|
|
74
|
-
source_count: num(raw.source_count, sources.length),
|
|
75
|
-
error_message: str(raw.error_message, raw.error),
|
|
76
|
-
started_at: str(raw.started_at),
|
|
77
|
-
finished_at: str(raw.finished_at),
|
|
78
|
-
created_at: str(raw.created_at),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface UsePendingOpsOptions {
|
|
83
|
-
/** Called once an op flips into a terminal state (done|error). */
|
|
84
|
-
onSettled?: (op: PendingOp) => void;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function usePendingOps(
|
|
88
|
-
config: ExplorerConfig,
|
|
89
|
-
api: FileApi,
|
|
90
|
-
opts: UsePendingOpsOptions = {},
|
|
91
|
-
) {
|
|
92
|
-
void config; // kept for future per-instance config knobs
|
|
93
|
-
const ops = ref<PendingOp[]>([]);
|
|
94
|
-
const announced = new Set<number>();
|
|
95
|
-
const settledAt = new Map<number, number>();
|
|
96
|
-
// First poll after mount returns the server's recent-history window
|
|
97
|
-
// (last 5 min of done/error rows). Mark them announced silently and
|
|
98
|
-
// skip the callback so we don't double-fire toasts on F5.
|
|
99
|
-
let firstPollDone = false;
|
|
100
|
-
|
|
101
|
-
const hasActive = computed(() =>
|
|
102
|
-
ops.value.some((o) => o.status === 'pending' || o.status === 'running'),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
106
|
-
|
|
107
|
-
function startPolling() {
|
|
108
|
-
if (timer) return;
|
|
109
|
-
if (!api.endpoints.opsList) return;
|
|
110
|
-
timer = setInterval(() => {
|
|
111
|
-
void poll();
|
|
112
|
-
}, POLL_MS);
|
|
113
|
-
// Kick a poll right away.
|
|
114
|
-
void poll();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function stopPolling() {
|
|
118
|
-
if (timer) {
|
|
119
|
-
clearInterval(timer);
|
|
120
|
-
timer = undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function poll(): Promise<void> {
|
|
125
|
-
if (!api.endpoints.opsList) return;
|
|
126
|
-
try {
|
|
127
|
-
const res = await api.jsonFetch<{ ops: Record<string, unknown>[] }>(api.endpoints.opsList);
|
|
128
|
-
const incoming = (res.ops || []).map(normalizeOp);
|
|
129
|
-
|
|
130
|
-
for (const op of incoming) {
|
|
131
|
-
if ((op.status === 'done' || op.status === 'error') && !announced.has(op.id)) {
|
|
132
|
-
announced.add(op.id);
|
|
133
|
-
settledAt.set(op.id, Date.now());
|
|
134
|
-
if (firstPollDone) {
|
|
135
|
-
opts.onSettled?.(op);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Filter incoming: pending/running always; settled only if user
|
|
141
|
-
// is currently seeing them in the local tray.
|
|
142
|
-
const localIds = new Set(ops.value.map((o) => o.id));
|
|
143
|
-
const visibleIncoming = incoming.filter((op) => {
|
|
144
|
-
if (op.status === 'pending' || op.status === 'running') return true;
|
|
145
|
-
if (!firstPollDone) return false;
|
|
146
|
-
return localIds.has(op.id);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const visibleIds = new Set(visibleIncoming.map((o) => o.id));
|
|
150
|
-
const local = ops.value.filter((o) => !visibleIds.has(o.id));
|
|
151
|
-
const merged = [...visibleIncoming, ...local];
|
|
152
|
-
|
|
153
|
-
firstPollDone = true;
|
|
154
|
-
|
|
155
|
-
// Sweep: drop terminal ops past RETAIN_MS.
|
|
156
|
-
const now = Date.now();
|
|
157
|
-
const after = merged.filter((o) => {
|
|
158
|
-
if (o.status === 'pending' || o.status === 'running') return true;
|
|
159
|
-
const at = settledAt.get(o.id);
|
|
160
|
-
return at !== undefined && now - at < RETAIN_MS;
|
|
161
|
-
});
|
|
162
|
-
ops.value = after;
|
|
163
|
-
|
|
164
|
-
if (!hasActive.value && after.length === 0) {
|
|
165
|
-
stopPolling();
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
// eslint-disable-next-line no-console
|
|
169
|
-
console.warn('[usePendingOps] poll failed:', err);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function register(raw: PendingOp | Record<string, unknown>): void {
|
|
174
|
-
const op = normalizeOp(raw as Record<string, unknown>);
|
|
175
|
-
const exists = ops.value.some((o) => o.id === op.id);
|
|
176
|
-
if (!exists) {
|
|
177
|
-
ops.value = [op, ...ops.value];
|
|
178
|
-
}
|
|
179
|
-
startPolling();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function dismiss(id: number): void {
|
|
183
|
-
ops.value = ops.value.filter((o) => o.id !== id);
|
|
184
|
-
settledAt.delete(id);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
onBeforeUnmount(() => {
|
|
188
|
-
stopPolling();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
ops,
|
|
193
|
-
hasActive,
|
|
194
|
-
register,
|
|
195
|
-
dismiss,
|
|
196
|
-
poll,
|
|
197
|
-
startPolling,
|
|
198
|
-
stopPolling,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* usePendingOps — track async copy/move/delete operations.
|
|
3
|
+
*
|
|
4
|
+
* Backend POST /files/{copy,move,delete} returns `{op}` immediately
|
|
5
|
+
* (status='pending') and the actual S3 work runs in a worker. The
|
|
6
|
+
* client polls `/files/ops` every 2s while there's anything still
|
|
7
|
+
* pending or running so the user gets live feedback ("3/5 kopyalandı").
|
|
8
|
+
*
|
|
9
|
+
* Polling is reference-counted: callers `register(op)` after starting a
|
|
10
|
+
* new op; an interval ticks while the registered set has any non-
|
|
11
|
+
* terminal entries. Terminal ops (done/error) stay in the local cache
|
|
12
|
+
* for `RETAIN_MS` so the UI can flash a final state before they fade
|
|
13
|
+
* out, then a sweep removes them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { computed, ref, onBeforeUnmount } from 'vue';
|
|
17
|
+
import type { ExplorerConfig } from '../types/ExplorerConfig';
|
|
18
|
+
import type { FileApi } from './useFileApi';
|
|
19
|
+
|
|
20
|
+
export interface PendingOp {
|
|
21
|
+
id: number;
|
|
22
|
+
op_type: 'copy' | 'move' | 'delete';
|
|
23
|
+
status: 'pending' | 'running' | 'done' | 'error';
|
|
24
|
+
progress_total: number;
|
|
25
|
+
progress_done: number;
|
|
26
|
+
target_path: string | null;
|
|
27
|
+
source_dir: string | null;
|
|
28
|
+
source_count: number;
|
|
29
|
+
error_message: string | null;
|
|
30
|
+
started_at: string | null;
|
|
31
|
+
finished_at: string | null;
|
|
32
|
+
created_at: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const POLL_MS = 2000;
|
|
36
|
+
const RETAIN_MS = 8000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* normalizeOp maps the backend ops row (`{kind,total,done,failed,error,dest,
|
|
40
|
+
* status:'ok'|'failed'|'partial'|…}`) onto the PendingOp contract the tray
|
|
41
|
+
* renders (`{op_type,progress_total,progress_done,error_message,status:'done'|
|
|
42
|
+
* 'error'|…}`). Without this the tray showed "undefined/undefined" for a
|
|
43
|
+
* running op (progress_* were undefined) and never treated an 'ok'/'failed'
|
|
44
|
+
* row as terminal. Tolerant of an already-normalized shape (idempotent).
|
|
45
|
+
*/
|
|
46
|
+
function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
47
|
+
const num = (...vals: unknown[]): number => {
|
|
48
|
+
for (const v of vals) if (typeof v === 'number') return v;
|
|
49
|
+
return 0;
|
|
50
|
+
};
|
|
51
|
+
const str = (...vals: unknown[]): string | null => {
|
|
52
|
+
for (const v of vals) if (typeof v === 'string' && v !== '') return v;
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
const rawStatus = String(raw.status ?? 'pending');
|
|
56
|
+
const status: PendingOp['status'] =
|
|
57
|
+
rawStatus === 'ok' || rawStatus === 'done'
|
|
58
|
+
? 'done'
|
|
59
|
+
: rawStatus === 'failed' || rawStatus === 'partial' || rawStatus === 'error'
|
|
60
|
+
? 'error'
|
|
61
|
+
: rawStatus === 'running'
|
|
62
|
+
? 'running'
|
|
63
|
+
: 'pending';
|
|
64
|
+
const sources = Array.isArray(raw.sources) ? raw.sources : [];
|
|
65
|
+
const opType = str(raw.op_type, raw.kind) ?? 'delete';
|
|
66
|
+
return {
|
|
67
|
+
id: num(raw.id),
|
|
68
|
+
op_type: (opType === 'copy' || opType === 'move' ? opType : 'delete') as PendingOp['op_type'],
|
|
69
|
+
status,
|
|
70
|
+
progress_total: num(raw.progress_total, raw.total, sources.length),
|
|
71
|
+
progress_done: num(raw.progress_done, raw.done),
|
|
72
|
+
target_path: str(raw.target_path, raw.dest),
|
|
73
|
+
source_dir: str(raw.source_dir),
|
|
74
|
+
source_count: num(raw.source_count, sources.length),
|
|
75
|
+
error_message: str(raw.error_message, raw.error),
|
|
76
|
+
started_at: str(raw.started_at),
|
|
77
|
+
finished_at: str(raw.finished_at),
|
|
78
|
+
created_at: str(raw.created_at),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UsePendingOpsOptions {
|
|
83
|
+
/** Called once an op flips into a terminal state (done|error). */
|
|
84
|
+
onSettled?: (op: PendingOp) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function usePendingOps(
|
|
88
|
+
config: ExplorerConfig,
|
|
89
|
+
api: FileApi,
|
|
90
|
+
opts: UsePendingOpsOptions = {},
|
|
91
|
+
) {
|
|
92
|
+
void config; // kept for future per-instance config knobs
|
|
93
|
+
const ops = ref<PendingOp[]>([]);
|
|
94
|
+
const announced = new Set<number>();
|
|
95
|
+
const settledAt = new Map<number, number>();
|
|
96
|
+
// First poll after mount returns the server's recent-history window
|
|
97
|
+
// (last 5 min of done/error rows). Mark them announced silently and
|
|
98
|
+
// skip the callback so we don't double-fire toasts on F5.
|
|
99
|
+
let firstPollDone = false;
|
|
100
|
+
|
|
101
|
+
const hasActive = computed(() =>
|
|
102
|
+
ops.value.some((o) => o.status === 'pending' || o.status === 'running'),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
106
|
+
|
|
107
|
+
function startPolling() {
|
|
108
|
+
if (timer) return;
|
|
109
|
+
if (!api.endpoints.opsList) return;
|
|
110
|
+
timer = setInterval(() => {
|
|
111
|
+
void poll();
|
|
112
|
+
}, POLL_MS);
|
|
113
|
+
// Kick a poll right away.
|
|
114
|
+
void poll();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopPolling() {
|
|
118
|
+
if (timer) {
|
|
119
|
+
clearInterval(timer);
|
|
120
|
+
timer = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function poll(): Promise<void> {
|
|
125
|
+
if (!api.endpoints.opsList) return;
|
|
126
|
+
try {
|
|
127
|
+
const res = await api.jsonFetch<{ ops: Record<string, unknown>[] }>(api.endpoints.opsList);
|
|
128
|
+
const incoming = (res.ops || []).map(normalizeOp);
|
|
129
|
+
|
|
130
|
+
for (const op of incoming) {
|
|
131
|
+
if ((op.status === 'done' || op.status === 'error') && !announced.has(op.id)) {
|
|
132
|
+
announced.add(op.id);
|
|
133
|
+
settledAt.set(op.id, Date.now());
|
|
134
|
+
if (firstPollDone) {
|
|
135
|
+
opts.onSettled?.(op);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Filter incoming: pending/running always; settled only if user
|
|
141
|
+
// is currently seeing them in the local tray.
|
|
142
|
+
const localIds = new Set(ops.value.map((o) => o.id));
|
|
143
|
+
const visibleIncoming = incoming.filter((op) => {
|
|
144
|
+
if (op.status === 'pending' || op.status === 'running') return true;
|
|
145
|
+
if (!firstPollDone) return false;
|
|
146
|
+
return localIds.has(op.id);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const visibleIds = new Set(visibleIncoming.map((o) => o.id));
|
|
150
|
+
const local = ops.value.filter((o) => !visibleIds.has(o.id));
|
|
151
|
+
const merged = [...visibleIncoming, ...local];
|
|
152
|
+
|
|
153
|
+
firstPollDone = true;
|
|
154
|
+
|
|
155
|
+
// Sweep: drop terminal ops past RETAIN_MS.
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const after = merged.filter((o) => {
|
|
158
|
+
if (o.status === 'pending' || o.status === 'running') return true;
|
|
159
|
+
const at = settledAt.get(o.id);
|
|
160
|
+
return at !== undefined && now - at < RETAIN_MS;
|
|
161
|
+
});
|
|
162
|
+
ops.value = after;
|
|
163
|
+
|
|
164
|
+
if (!hasActive.value && after.length === 0) {
|
|
165
|
+
stopPolling();
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.warn('[usePendingOps] poll failed:', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function register(raw: PendingOp | Record<string, unknown>): void {
|
|
174
|
+
const op = normalizeOp(raw as Record<string, unknown>);
|
|
175
|
+
const exists = ops.value.some((o) => o.id === op.id);
|
|
176
|
+
if (!exists) {
|
|
177
|
+
ops.value = [op, ...ops.value];
|
|
178
|
+
}
|
|
179
|
+
startPolling();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function dismiss(id: number): void {
|
|
183
|
+
ops.value = ops.value.filter((o) => o.id !== id);
|
|
184
|
+
settledAt.delete(id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
onBeforeUnmount(() => {
|
|
188
|
+
stopPolling();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ops,
|
|
193
|
+
hasActive,
|
|
194
|
+
register,
|
|
195
|
+
dismiss,
|
|
196
|
+
poll,
|
|
197
|
+
startPolling,
|
|
198
|
+
stopPolling,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useSelection — multi-select state with Ctrl/Shift semantics.
|
|
3
|
-
*
|
|
4
|
-
* click(path, {ctrl, shift})
|
|
5
|
-
* no-mod → replace selection with [path]
|
|
6
|
-
* ctrl → toggle path in selection
|
|
7
|
-
* shift → range select from anchor to path
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { ref, computed } from 'vue';
|
|
11
|
-
import type { FileNode } from '../types/FileNode';
|
|
12
|
-
|
|
13
|
-
export function useSelection(items: () => FileNode[]) {
|
|
14
|
-
const selected = ref<Set<string>>(new Set());
|
|
15
|
-
const anchor = ref<string | null>(null);
|
|
16
|
-
|
|
17
|
-
function click(path: string, mod: { ctrl?: boolean; shift?: boolean } = {}) {
|
|
18
|
-
if (mod.shift && anchor.value) {
|
|
19
|
-
const list = items();
|
|
20
|
-
const aIdx = list.findIndex((n) => n.path === anchor.value);
|
|
21
|
-
const bIdx = list.findIndex((n) => n.path === path);
|
|
22
|
-
if (aIdx === -1 || bIdx === -1) {
|
|
23
|
-
selected.value = new Set([path]);
|
|
24
|
-
anchor.value = path;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const [lo, hi] = aIdx < bIdx ? [aIdx, bIdx] : [bIdx, aIdx];
|
|
28
|
-
const next = new Set<string>();
|
|
29
|
-
for (let i = lo; i <= hi; i++) next.add(list[i].path);
|
|
30
|
-
selected.value = next;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (mod.ctrl) {
|
|
35
|
-
const next = new Set(selected.value);
|
|
36
|
-
if (next.has(path)) next.delete(path);
|
|
37
|
-
else next.add(path);
|
|
38
|
-
selected.value = next;
|
|
39
|
-
anchor.value = path;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
selected.value = new Set([path]);
|
|
44
|
-
anchor.value = path;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function clear() {
|
|
48
|
-
selected.value = new Set();
|
|
49
|
-
anchor.value = null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function selectAll() {
|
|
53
|
-
selected.value = new Set(items().map((n) => n.path));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function has(path: string): boolean {
|
|
57
|
-
return selected.value.has(path);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function add(path: string) {
|
|
61
|
-
const next = new Set(selected.value);
|
|
62
|
-
next.add(path);
|
|
63
|
-
selected.value = next;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function remove(path: string) {
|
|
67
|
-
const next = new Set(selected.value);
|
|
68
|
-
next.delete(path);
|
|
69
|
-
selected.value = next;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const size = computed(() => selected.value.size);
|
|
73
|
-
const isEmpty = computed(() => selected.value.size === 0);
|
|
74
|
-
|
|
75
|
-
const nodes = computed(() => {
|
|
76
|
-
const list = items();
|
|
77
|
-
return list.filter((n) => selected.value.has(n.path));
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return { selected, anchor, click, clear, selectAll, has, add, remove, size, isEmpty, nodes };
|
|
81
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* useSelection — multi-select state with Ctrl/Shift semantics.
|
|
3
|
+
*
|
|
4
|
+
* click(path, {ctrl, shift})
|
|
5
|
+
* no-mod → replace selection with [path]
|
|
6
|
+
* ctrl → toggle path in selection
|
|
7
|
+
* shift → range select from anchor to path
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ref, computed } from 'vue';
|
|
11
|
+
import type { FileNode } from '../types/FileNode';
|
|
12
|
+
|
|
13
|
+
export function useSelection(items: () => FileNode[]) {
|
|
14
|
+
const selected = ref<Set<string>>(new Set());
|
|
15
|
+
const anchor = ref<string | null>(null);
|
|
16
|
+
|
|
17
|
+
function click(path: string, mod: { ctrl?: boolean; shift?: boolean } = {}) {
|
|
18
|
+
if (mod.shift && anchor.value) {
|
|
19
|
+
const list = items();
|
|
20
|
+
const aIdx = list.findIndex((n) => n.path === anchor.value);
|
|
21
|
+
const bIdx = list.findIndex((n) => n.path === path);
|
|
22
|
+
if (aIdx === -1 || bIdx === -1) {
|
|
23
|
+
selected.value = new Set([path]);
|
|
24
|
+
anchor.value = path;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const [lo, hi] = aIdx < bIdx ? [aIdx, bIdx] : [bIdx, aIdx];
|
|
28
|
+
const next = new Set<string>();
|
|
29
|
+
for (let i = lo; i <= hi; i++) next.add(list[i].path);
|
|
30
|
+
selected.value = next;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (mod.ctrl) {
|
|
35
|
+
const next = new Set(selected.value);
|
|
36
|
+
if (next.has(path)) next.delete(path);
|
|
37
|
+
else next.add(path);
|
|
38
|
+
selected.value = next;
|
|
39
|
+
anchor.value = path;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
selected.value = new Set([path]);
|
|
44
|
+
anchor.value = path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clear() {
|
|
48
|
+
selected.value = new Set();
|
|
49
|
+
anchor.value = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function selectAll() {
|
|
53
|
+
selected.value = new Set(items().map((n) => n.path));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function has(path: string): boolean {
|
|
57
|
+
return selected.value.has(path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function add(path: string) {
|
|
61
|
+
const next = new Set(selected.value);
|
|
62
|
+
next.add(path);
|
|
63
|
+
selected.value = next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function remove(path: string) {
|
|
67
|
+
const next = new Set(selected.value);
|
|
68
|
+
next.delete(path);
|
|
69
|
+
selected.value = next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const size = computed(() => selected.value.size);
|
|
73
|
+
const isEmpty = computed(() => selected.value.size === 0);
|
|
74
|
+
|
|
75
|
+
const nodes = computed(() => {
|
|
76
|
+
const list = items();
|
|
77
|
+
return list.filter((n) => selected.value.has(n.path));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { selected, anchor, click, clear, selectAll, has, add, remove, size, isEmpty, nodes };
|
|
81
|
+
}
|