@brftech/filex-core 0.4.2 → 0.5.0
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/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-DKW-HyXT.js} +2 -2
- package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-DKW-HyXT.js.map} +1 -1
- package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-D2HmP2yr.js} +2 -2
- package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-D2HmP2yr.js.map} +1 -1
- package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-DPNvOFCs.js} +2 -2
- package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-DPNvOFCs.js.map} +1 -1
- package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-ByqjNuNL.js} +2 -2
- package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-ByqjNuNL.js.map} +1 -1
- package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-2iONhbuJ.js} +2 -2
- package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-2iONhbuJ.js.map} +1 -1
- package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-CmiU7hIT.js} +2 -2
- package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-CmiU7hIT.js.map} +1 -1
- package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Inhl2yN9.js} +2 -2
- package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Inhl2yN9.js.map} +1 -1
- package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-C2AFFEze.js} +2 -2
- package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-C2AFFEze.js.map} +1 -1
- package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-YdW14q_n.js} +2 -2
- package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-YdW14q_n.js.map} +1 -1
- package/dist/filex-core.js +47 -25
- package/dist/filex-core.umd.cjs +40 -40
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-eARevpz0.js +9952 -0
- package/dist/index-eARevpz0.js.map +1 -0
- package/dist/index.d.ts +363 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +269 -1
- package/src/components/CommandPalette.vue +12 -0
- package/src/components/ContextMenu.vue +114 -8
- package/src/components/GridView.vue +33 -1
- package/src/components/ListView.vue +39 -6
- package/src/components/OnboardingTour.vue +332 -0
- package/src/components/OperationsCenter.vue +284 -0
- package/src/components/PendingOpsTray.vue +50 -66
- package/src/components/QuickLook.vue +142 -0
- package/src/components/ShortcutSettings.vue +270 -0
- package/src/components/ShortcutsHelp.vue +30 -9
- package/src/components/ThemeGallery.vue +253 -0
- package/src/components/Toolbar.vue +52 -6
- package/src/components/UploadProgress.vue +49 -57
- package/src/composables/useKeyboardShortcuts.ts +321 -137
- package/src/composables/useOperations.ts +291 -0
- package/src/index.ts +45 -0
- package/src/lib/dragGhost.ts +41 -0
- package/src/lib/themes.ts +505 -0
- package/src/locales/en.ts +92 -0
- package/src/locales/tr.ts +92 -0
- package/src/modals/Modal.vue +41 -5
- package/src/styles/base.css +672 -0
- package/dist/index-BLocdbmc.js +0 -7993
- package/dist/index-BLocdbmc.js.map +0 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* OperationsCenter — the single visible surface for long-running work
|
|
4
|
+
* (wiring:c3). Compact bottom-right badge (active count + aggregate
|
|
5
|
+
* progress ring; hidden when idle) that expands into a panel: active
|
|
6
|
+
* operations on top, session history below.
|
|
7
|
+
*
|
|
8
|
+
* Renders whatever the useOperations store aggregates — uploads, ops-queue
|
|
9
|
+
* jobs, future convert/archive kinds — with one uniform row template.
|
|
10
|
+
* Cancel / retry / dismiss route back through the store's action registry
|
|
11
|
+
* to the owning publisher.
|
|
12
|
+
*/
|
|
13
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
14
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
15
|
+
import type { Operation, OperationsStore } from '../composables/useOperations';
|
|
16
|
+
import { useLocale } from '../composables/useLocale';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
center: OperationsStore;
|
|
20
|
+
locale: LocaleCode;
|
|
21
|
+
narrow?: boolean;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const { t, formatSize } = useLocale(() => props.locale);
|
|
25
|
+
|
|
26
|
+
const open = ref(false);
|
|
27
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
28
|
+
|
|
29
|
+
const active = computed(() => props.center.active.value);
|
|
30
|
+
const history = computed(() => props.center.history.value);
|
|
31
|
+
const hasError = computed(() => props.center.hasError.value);
|
|
32
|
+
const overall = computed(() => props.center.overallPercent.value);
|
|
33
|
+
const runningCount = computed(() => props.center.runningCount.value);
|
|
34
|
+
|
|
35
|
+
// Badge visible while anything is active; once the panel is open it stays
|
|
36
|
+
// up (showing history) until the user closes it. Idle + closed → hidden.
|
|
37
|
+
const visible = computed(() => active.value.length > 0 || (open.value && history.value.length > 0));
|
|
38
|
+
|
|
39
|
+
watch(visible, (v) => {
|
|
40
|
+
if (!v) open.value = false;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ---- progress ring ----
|
|
44
|
+
const RING_R = 8.5;
|
|
45
|
+
const RING_C = 2 * Math.PI * RING_R;
|
|
46
|
+
const ringOffset = computed(() => {
|
|
47
|
+
const p = overall.value;
|
|
48
|
+
if (p === null) return RING_C * 0.72; // spinner arc
|
|
49
|
+
return RING_C * (1 - Math.min(100, Math.max(0, p)) / 100);
|
|
50
|
+
});
|
|
51
|
+
const ringSpin = computed(() => overall.value === null && runningCount.value > 0);
|
|
52
|
+
|
|
53
|
+
const pctText = computed(() => {
|
|
54
|
+
if (runningCount.value === 0 || overall.value === null) return '';
|
|
55
|
+
return t('opc.percent', { n: overall.value });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const badgeAria = computed(() => t('opc.aria_badge', { n: active.value.length }));
|
|
59
|
+
|
|
60
|
+
// ---- row helpers ----
|
|
61
|
+
const KIND_ICONS: Record<Operation['kind'], string> = {
|
|
62
|
+
upload: 'M12 16V5M7 9.5 12 5l5 4.5M5 19h14',
|
|
63
|
+
copy: 'M9 9h11v11H9zM5 15V4h11',
|
|
64
|
+
move: 'M4 12h13M12 6l6 6-6 6',
|
|
65
|
+
delete: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13',
|
|
66
|
+
convert: 'M20 8A8 8 0 0 0 6 6L4 8M4 16a8 8 0 0 0 14 2l2-2M20 3v5h-5M4 21v-5h5',
|
|
67
|
+
archive: 'M4 8V5h16v3zM5 8h14v12H5zM10 12h4',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function kindLabel(o: Operation): string {
|
|
71
|
+
return t(`opc.kind.${o.kind}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rowTitle(o: Operation): string {
|
|
75
|
+
return o.name || kindLabel(o);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function statusText(o: Operation): string {
|
|
79
|
+
if (o.status === 'running') {
|
|
80
|
+
if (o.queued) return t('opc.queued');
|
|
81
|
+
if (o.totalBytes && o.totalBytes > 0) {
|
|
82
|
+
return `${formatSize(o.uploadedBytes ?? 0)} / ${formatSize(o.totalBytes)}`;
|
|
83
|
+
}
|
|
84
|
+
if (o.totalCount && o.totalCount > 0) {
|
|
85
|
+
return `${o.doneCount ?? 0}/${o.totalCount}`;
|
|
86
|
+
}
|
|
87
|
+
return t('opc.status.running');
|
|
88
|
+
}
|
|
89
|
+
if (o.status === 'done') return t('opc.status.done');
|
|
90
|
+
if (o.status === 'aborted') return t('opc.status.aborted');
|
|
91
|
+
return o.error || t('opc.status.error');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function chipText(o: Operation): string {
|
|
95
|
+
return t(`opc.status.${o.status === 'running' ? 'running' : o.status}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- outside click + Escape ----
|
|
99
|
+
function onDocPointerDown(ev: MouseEvent) {
|
|
100
|
+
if (!open.value) return;
|
|
101
|
+
const el = rootEl.value;
|
|
102
|
+
if (el && ev.target instanceof Node && !el.contains(ev.target)) open.value = false;
|
|
103
|
+
}
|
|
104
|
+
function onDocKeydown(ev: KeyboardEvent) {
|
|
105
|
+
if (open.value && ev.key === 'Escape') open.value = false;
|
|
106
|
+
}
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
document.addEventListener('mousedown', onDocPointerDown);
|
|
109
|
+
document.addEventListener('keydown', onDocKeydown);
|
|
110
|
+
});
|
|
111
|
+
onBeforeUnmount(() => {
|
|
112
|
+
document.removeEventListener('mousedown', onDocPointerDown);
|
|
113
|
+
document.removeEventListener('keydown', onDocKeydown);
|
|
114
|
+
});
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<div
|
|
119
|
+
v-if="visible"
|
|
120
|
+
ref="rootEl"
|
|
121
|
+
class="fe-opc"
|
|
122
|
+
:class="{ 'fe-opc--narrow': narrow, 'fe-opc--open': open }"
|
|
123
|
+
>
|
|
124
|
+
<transition name="fe-opc-pop">
|
|
125
|
+
<section v-if="open" class="fe-opc__panel" role="dialog" :aria-label="t('opc.title')">
|
|
126
|
+
<header class="fe-opc__head">
|
|
127
|
+
<strong>{{ t('opc.title') }}</strong>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="fe-opc__x"
|
|
131
|
+
:aria-label="t('opc.close')"
|
|
132
|
+
@click="open = false"
|
|
133
|
+
>×</button>
|
|
134
|
+
</header>
|
|
135
|
+
<div class="fe-opc__body">
|
|
136
|
+
<div v-if="active.length > 0" class="fe-opc__section" aria-live="polite">
|
|
137
|
+
<h3 class="fe-opc__section-title">{{ t('opc.active') }}</h3>
|
|
138
|
+
<ul class="fe-opc__list">
|
|
139
|
+
<li
|
|
140
|
+
v-for="o in active"
|
|
141
|
+
:key="o.key"
|
|
142
|
+
class="fe-opc__item"
|
|
143
|
+
:class="`is-${o.status}`"
|
|
144
|
+
>
|
|
145
|
+
<svg
|
|
146
|
+
class="fe-opc__kicon"
|
|
147
|
+
viewBox="0 0 24 24"
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
stroke-width="1.8"
|
|
151
|
+
stroke-linecap="round"
|
|
152
|
+
stroke-linejoin="round"
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
focusable="false"
|
|
155
|
+
>
|
|
156
|
+
<path :d="KIND_ICONS[o.kind]" />
|
|
157
|
+
</svg>
|
|
158
|
+
<div class="fe-opc__mid">
|
|
159
|
+
<div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
|
|
160
|
+
<div class="fe-opc__sub">
|
|
161
|
+
<span class="fe-opc__kind">{{ kindLabel(o) }}</span>
|
|
162
|
+
<span class="fe-opc__dot" aria-hidden="true">·</span>
|
|
163
|
+
<span class="fe-opc__state">{{ statusText(o) }}</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div
|
|
166
|
+
v-if="o.status === 'running' && o.percent !== null"
|
|
167
|
+
class="fe-opc__bar"
|
|
168
|
+
role="progressbar"
|
|
169
|
+
:aria-valuenow="o.percent"
|
|
170
|
+
aria-valuemin="0"
|
|
171
|
+
aria-valuemax="100"
|
|
172
|
+
>
|
|
173
|
+
<div class="fe-opc__bar-fill" :style="{ width: o.percent + '%' }" />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<span
|
|
177
|
+
v-if="o.status === 'running' && o.percent === null && !o.queued"
|
|
178
|
+
class="fe-opc__spin"
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
/>
|
|
181
|
+
<div class="fe-opc__acts">
|
|
182
|
+
<button
|
|
183
|
+
v-if="o.status === 'running' && o.cancellable"
|
|
184
|
+
type="button"
|
|
185
|
+
class="fe-opc__act"
|
|
186
|
+
@click="center.cancel(o.key)"
|
|
187
|
+
>{{ t('opc.cancel') }}</button>
|
|
188
|
+
<button
|
|
189
|
+
v-if="o.status === 'error' && o.retryable"
|
|
190
|
+
type="button"
|
|
191
|
+
class="fe-opc__act fe-opc__act--retry"
|
|
192
|
+
@click="center.retry(o.key)"
|
|
193
|
+
>{{ t('opc.retry') }}</button>
|
|
194
|
+
<button
|
|
195
|
+
v-if="o.status !== 'running'"
|
|
196
|
+
type="button"
|
|
197
|
+
class="fe-opc__x"
|
|
198
|
+
:aria-label="t('opc.dismiss')"
|
|
199
|
+
@click="center.dismiss(o.key)"
|
|
200
|
+
>×</button>
|
|
201
|
+
</div>
|
|
202
|
+
</li>
|
|
203
|
+
</ul>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div v-if="history.length > 0" class="fe-opc__section">
|
|
207
|
+
<h3 class="fe-opc__section-title">
|
|
208
|
+
{{ t('opc.history') }}
|
|
209
|
+
<button type="button" class="fe-opc__act" @click="center.clearHistory()">
|
|
210
|
+
{{ t('opc.clear') }}
|
|
211
|
+
</button>
|
|
212
|
+
</h3>
|
|
213
|
+
<ul class="fe-opc__list fe-opc__list--history">
|
|
214
|
+
<li
|
|
215
|
+
v-for="o in history"
|
|
216
|
+
:key="'h-' + o.key"
|
|
217
|
+
class="fe-opc__item fe-opc__item--history"
|
|
218
|
+
:class="`is-${o.status}`"
|
|
219
|
+
>
|
|
220
|
+
<svg
|
|
221
|
+
class="fe-opc__kicon"
|
|
222
|
+
viewBox="0 0 24 24"
|
|
223
|
+
fill="none"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
stroke-width="1.8"
|
|
226
|
+
stroke-linecap="round"
|
|
227
|
+
stroke-linejoin="round"
|
|
228
|
+
aria-hidden="true"
|
|
229
|
+
focusable="false"
|
|
230
|
+
>
|
|
231
|
+
<path :d="KIND_ICONS[o.kind]" />
|
|
232
|
+
</svg>
|
|
233
|
+
<div class="fe-opc__mid">
|
|
234
|
+
<div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
|
|
235
|
+
<div v-if="o.status === 'error' && o.error" class="fe-opc__sub fe-opc__sub--err">
|
|
236
|
+
{{ o.error }}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
<span class="fe-opc__chip" :class="`is-${o.status}`">{{ chipText(o) }}</span>
|
|
240
|
+
</li>
|
|
241
|
+
</ul>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<p v-if="active.length === 0 && history.length === 0" class="fe-opc__empty">
|
|
245
|
+
{{ t('opc.empty') }}
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
</section>
|
|
249
|
+
</transition>
|
|
250
|
+
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
class="fe-opc__badge"
|
|
254
|
+
:class="{ 'is-error': hasError }"
|
|
255
|
+
:aria-expanded="open"
|
|
256
|
+
:aria-label="badgeAria"
|
|
257
|
+
@click="open = !open"
|
|
258
|
+
>
|
|
259
|
+
<svg
|
|
260
|
+
class="fe-opc__ring"
|
|
261
|
+
:class="{ 'fe-opc__ring--spin': ringSpin }"
|
|
262
|
+
viewBox="0 0 22 22"
|
|
263
|
+
aria-hidden="true"
|
|
264
|
+
focusable="false"
|
|
265
|
+
>
|
|
266
|
+
<circle class="fe-opc__ring-track" cx="11" cy="11" :r="RING_R" fill="none" stroke-width="2.5" />
|
|
267
|
+
<circle
|
|
268
|
+
class="fe-opc__ring-fill"
|
|
269
|
+
cx="11"
|
|
270
|
+
cy="11"
|
|
271
|
+
:r="RING_R"
|
|
272
|
+
fill="none"
|
|
273
|
+
stroke-width="2.5"
|
|
274
|
+
stroke-linecap="round"
|
|
275
|
+
:stroke-dasharray="RING_C"
|
|
276
|
+
:stroke-dashoffset="ringOffset"
|
|
277
|
+
transform="rotate(-90 11 11)"
|
|
278
|
+
/>
|
|
279
|
+
</svg>
|
|
280
|
+
<span class="fe-opc__count">{{ active.length }}</span>
|
|
281
|
+
<span v-if="pctText" class="fe-opc__pct">{{ pctText }}</span>
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
</template>
|
|
@@ -1,91 +1,75 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* PendingOpsTray —
|
|
3
|
+
* PendingOpsTray — renderless ops-queue publisher (wiring:c3).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Historically a bottom-left toast list for queued copy/move/delete. Since
|
|
6
|
+
* the unified operations center it renders NOTHING: it reconciles the
|
|
7
|
+
* usePendingOps rows into the shared useOperations store; OperationsCenter
|
|
8
|
+
* draws them. Terminal-state lingering ("bitti" flash → history) and sticky
|
|
9
|
+
* errors are handled by the store — note usePendingOps sweeps terminal rows
|
|
10
|
+
* after its RETAIN window, which the store treats as "move to history"
|
|
11
|
+
* (errors stay pinned until the user dismisses them).
|
|
12
|
+
*
|
|
13
|
+
* Queue ops are not client-retryable: the normalized PendingOp no longer
|
|
14
|
+
* carries the source list, so a failed op only offers dismiss.
|
|
10
15
|
*/
|
|
11
|
-
import {
|
|
16
|
+
import { watch } from 'vue';
|
|
12
17
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
13
18
|
import type { PendingOp } from '../composables/usePendingOps';
|
|
14
|
-
import {
|
|
19
|
+
import type { OperationsStore, OperationStatus } from '../composables/useOperations';
|
|
15
20
|
|
|
16
21
|
const props = defineProps<{
|
|
17
22
|
ops: PendingOp[];
|
|
23
|
+
/** Kept for mount compatibility — strings now render in OperationsCenter. */
|
|
18
24
|
locale: LocaleCode;
|
|
25
|
+
center: OperationsStore;
|
|
19
26
|
}>();
|
|
20
27
|
|
|
21
28
|
const emit = defineEmits<{
|
|
22
29
|
(e: 'dismiss', id: number): void;
|
|
23
30
|
}>();
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
function mapStatus(op: PendingOp): OperationStatus {
|
|
33
|
+
if (op.status === 'done') return 'done';
|
|
34
|
+
if (op.status === 'error') return 'error';
|
|
35
|
+
return 'running'; // pending | running
|
|
36
|
+
}
|
|
28
37
|
|
|
29
|
-
function
|
|
38
|
+
function percentOf(op: PendingOp): number | null {
|
|
30
39
|
if (op.status === 'done') return 100;
|
|
31
|
-
if (op.progress_total <= 0) return
|
|
40
|
+
if (op.progress_total <= 0) return null;
|
|
32
41
|
return Math.min(100, Math.round((op.progress_done / op.progress_total) * 100));
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
44
|
+
watch(
|
|
45
|
+
() => props.ops,
|
|
46
|
+
(ops) => {
|
|
47
|
+
props.center.sync(
|
|
48
|
+
'ops',
|
|
49
|
+
ops.map((op) => ({
|
|
50
|
+
input: {
|
|
51
|
+
id: op.id,
|
|
52
|
+
kind: op.op_type,
|
|
53
|
+
name: op.target_path || op.source_dir || '',
|
|
54
|
+
percent: percentOf(op),
|
|
55
|
+
status: mapStatus(op),
|
|
56
|
+
error: op.error_message,
|
|
57
|
+
queued: op.status === 'pending',
|
|
58
|
+
doneCount: op.progress_done,
|
|
59
|
+
totalCount: op.progress_total,
|
|
60
|
+
cancellable: false,
|
|
61
|
+
retryable: false,
|
|
62
|
+
},
|
|
63
|
+
actions: {
|
|
64
|
+
dismiss: () => emit('dismiss', op.id),
|
|
65
|
+
},
|
|
66
|
+
})),
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
{ immediate: true },
|
|
70
|
+
);
|
|
54
71
|
</script>
|
|
55
72
|
|
|
56
73
|
<template>
|
|
57
|
-
<
|
|
58
|
-
v-if="sorted.length > 0"
|
|
59
|
-
name="fe-ops-slide"
|
|
60
|
-
tag="div"
|
|
61
|
-
class="fe-ops"
|
|
62
|
-
role="status"
|
|
63
|
-
aria-live="polite"
|
|
64
|
-
>
|
|
65
|
-
<div
|
|
66
|
-
v-for="op in sorted"
|
|
67
|
-
:key="op.id"
|
|
68
|
-
class="fe-ops__item"
|
|
69
|
-
:class="statusClass(op)"
|
|
70
|
-
>
|
|
71
|
-
<div class="fe-ops__row">
|
|
72
|
-
<span class="fe-ops__title" :title="op.target_path || op.source_dir || ''">
|
|
73
|
-
{{ summary(op) }}
|
|
74
|
-
</span>
|
|
75
|
-
<button
|
|
76
|
-
v-if="op.status === 'done' || op.status === 'error'"
|
|
77
|
-
type="button"
|
|
78
|
-
class="fe-ops__dismiss"
|
|
79
|
-
aria-label="Dismiss"
|
|
80
|
-
@click="emit('dismiss', op.id)"
|
|
81
|
-
>×</button>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="fe-ops__bar">
|
|
84
|
-
<div class="fe-ops__bar-fill" :style="{ width: percent(op) + '%' }" />
|
|
85
|
-
</div>
|
|
86
|
-
<div v-if="op.status === 'error' && op.error_message" class="fe-ops__error">
|
|
87
|
-
{{ op.error_message }}
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</transition-group>
|
|
74
|
+
<i v-if="false" />
|
|
91
75
|
</template>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* QuickLook — wiring:c2 Space quick-look overlay.
|
|
4
|
+
*
|
|
5
|
+
* A thin behavioural wrapper over the existing PreviewModal (view
|
|
6
|
+
* mode), so every viewer the modal knows (image/video/pdf/office/code/
|
|
7
|
+
* 3D/archive/…) works in quick-look for free. What this layer adds:
|
|
8
|
+
*
|
|
9
|
+
* - Space closes again (macOS Quick Look convention)
|
|
10
|
+
* - ← ↑ / → ↓ ask the host to move the selection; the host keeps the
|
|
11
|
+
* `file` prop in sync so the preview follows the selection
|
|
12
|
+
* - Enter promotes the peek into the full open flow (emit 'open-full')
|
|
13
|
+
* - a small floating hint bar with the key legend
|
|
14
|
+
*
|
|
15
|
+
* Keys are handled in window CAPTURE phase with stopPropagation so the
|
|
16
|
+
* global shortcut registry never double-handles them; events that
|
|
17
|
+
* originate in a form control inside a viewer (e.g. the CSV filter
|
|
18
|
+
* input) are left alone. Esc is untouched — PreviewModal's own Modal
|
|
19
|
+
* already closes on it.
|
|
20
|
+
*/
|
|
21
|
+
import { onBeforeUnmount, watch } from 'vue';
|
|
22
|
+
import type { FileNode } from '../types/FileNode';
|
|
23
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
|
+
import { useLocale } from '../composables/useLocale';
|
|
25
|
+
import PreviewModal from '../modals/PreviewModal.vue';
|
|
26
|
+
|
|
27
|
+
const props = defineProps<{
|
|
28
|
+
open: boolean;
|
|
29
|
+
locale: LocaleCode;
|
|
30
|
+
file: FileNode | null;
|
|
31
|
+
previewUrl: (path: string) => string;
|
|
32
|
+
downloadUrl: (path: string) => string;
|
|
33
|
+
onlyOfficeBase?: string | null;
|
|
34
|
+
onlyOfficeConfigEndpoint?: string | null;
|
|
35
|
+
authHeaders?: () => Record<string, string>;
|
|
36
|
+
authCredentials?: RequestCredentials;
|
|
37
|
+
drawioUrl?: string | null;
|
|
38
|
+
pdfWorkerUrl?: string | null;
|
|
39
|
+
viewerBaseUrl?: string | null;
|
|
40
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
(e: 'close'): void;
|
|
45
|
+
/** Arrow navigation: -1 = previous file, +1 = next file. */
|
|
46
|
+
(e: 'nav', delta: number): void;
|
|
47
|
+
/** Enter — close the peek and open the file for real. */
|
|
48
|
+
(e: 'open-full'): void;
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
const { t } = useLocale(() => props.locale);
|
|
52
|
+
|
|
53
|
+
function inFormControl(target: EventTarget | null): boolean {
|
|
54
|
+
const el = target as HTMLElement | null;
|
|
55
|
+
return !!(
|
|
56
|
+
el &&
|
|
57
|
+
(el.tagName === 'INPUT' ||
|
|
58
|
+
el.tagName === 'TEXTAREA' ||
|
|
59
|
+
el.tagName === 'SELECT' ||
|
|
60
|
+
el.isContentEditable)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(e: KeyboardEvent) {
|
|
65
|
+
if (!props.open) return;
|
|
66
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
67
|
+
if (inFormControl(e.target)) return;
|
|
68
|
+
switch (e.key) {
|
|
69
|
+
case ' ':
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
emit('close');
|
|
73
|
+
break;
|
|
74
|
+
case 'ArrowRight':
|
|
75
|
+
case 'ArrowDown':
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
emit('nav', 1);
|
|
79
|
+
break;
|
|
80
|
+
case 'ArrowLeft':
|
|
81
|
+
case 'ArrowUp':
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
emit('nav', -1);
|
|
85
|
+
break;
|
|
86
|
+
case 'Enter':
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
emit('open-full');
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
watch(
|
|
95
|
+
() => props.open,
|
|
96
|
+
(open) => {
|
|
97
|
+
if (open) window.addEventListener('keydown', onKeydown, true);
|
|
98
|
+
else window.removeEventListener('keydown', onKeydown, true);
|
|
99
|
+
},
|
|
100
|
+
{ immediate: true },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<PreviewModal
|
|
108
|
+
class="fe-quicklook"
|
|
109
|
+
:open="open"
|
|
110
|
+
:locale="locale"
|
|
111
|
+
:file="file"
|
|
112
|
+
:theme="theme"
|
|
113
|
+
:preview-url="previewUrl"
|
|
114
|
+
:download-url="downloadUrl"
|
|
115
|
+
:only-office-base="onlyOfficeBase"
|
|
116
|
+
:only-office-config-endpoint="onlyOfficeConfigEndpoint"
|
|
117
|
+
open-mode="view"
|
|
118
|
+
:auth-headers="authHeaders"
|
|
119
|
+
:auth-credentials="authCredentials"
|
|
120
|
+
:drawio-url="drawioUrl"
|
|
121
|
+
:pdf-worker-url="pdfWorkerUrl"
|
|
122
|
+
:viewer-base-url="viewerBaseUrl"
|
|
123
|
+
@close="emit('close')"
|
|
124
|
+
/>
|
|
125
|
+
<transition name="fe-toast">
|
|
126
|
+
<div
|
|
127
|
+
v-if="open"
|
|
128
|
+
class="fe fe-ql-hint"
|
|
129
|
+
:class="{
|
|
130
|
+
'fe--theme-light': theme === 'light',
|
|
131
|
+
'fe--theme-dark': theme === 'dark',
|
|
132
|
+
}"
|
|
133
|
+
aria-hidden="true"
|
|
134
|
+
>
|
|
135
|
+
<kbd class="fe-kbd">Space</kbd> {{ t('quicklook.hint_close') }}
|
|
136
|
+
<span class="fe-ql-hint__sep">·</span>
|
|
137
|
+
<kbd class="fe-kbd">↑</kbd><kbd class="fe-kbd">↓</kbd> {{ t('quicklook.hint_nav') }}
|
|
138
|
+
<span class="fe-ql-hint__sep">·</span>
|
|
139
|
+
<kbd class="fe-kbd">Enter</kbd> {{ t('quicklook.hint_open') }}
|
|
140
|
+
</div>
|
|
141
|
+
</transition>
|
|
142
|
+
</template>
|