@brftech/filex-core 0.5.0 → 0.7.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-DKW-HyXT.js → ArchiveViewer-w6GDJJt9.js} +2 -2
- package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-w6GDJJt9.js.map} +1 -1
- package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-Ci5-mke8.js} +2 -2
- package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-Ci5-mke8.js.map} +1 -1
- package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-D2iMuQfD.js} +2 -2
- package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-D2iMuQfD.js.map} +1 -1
- package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-D4ydsa9X.js} +2 -2
- package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-D4ydsa9X.js.map} +1 -1
- package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-CWBwiFKX.js} +2 -2
- package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-CWBwiFKX.js.map} +1 -1
- package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-CDhuqQli.js} +2 -2
- package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-CDhuqQli.js.map} +1 -1
- package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-C_TkFb2H.js} +2 -2
- package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-C_TkFb2H.js.map} +1 -1
- package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-BmWCXR7e.js} +2 -2
- package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-BmWCXR7e.js.map} +1 -1
- package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-5kPT1QsY.js} +2 -2
- package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-5kPT1QsY.js.map} +1 -1
- package/dist/filex-core.js +66 -47
- package/dist/filex-core.umd.cjs +41 -40
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-nc0-oI_l.js +11723 -0
- package/dist/index-nc0-oI_l.js.map +1 -0
- package/dist/index.d.ts +364 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +756 -23
- package/src/components/CommandPalette.vue +12 -1
- package/src/components/EncryptedFolderModal.vue +125 -0
- package/src/components/GalleryView.vue +215 -0
- package/src/components/GridView.vue +2 -0
- package/src/components/InspectorPanel.vue +170 -0
- package/src/components/ListView.vue +2 -0
- package/src/components/SecondaryPane.vue +411 -0
- package/src/components/TabBar.vue +174 -0
- package/src/components/Toolbar.vue +23 -5
- package/src/composables/useFileApi.ts +60 -0
- package/src/composables/useKeyboardShortcuts.ts +19 -0
- package/src/composables/useTabs.ts +222 -0
- package/src/index.ts +27 -0
- package/src/lib/e2ecrypto.ts +299 -0
- package/src/locales/en.ts +67 -0
- package/src/locales/tr.ts +67 -0
- package/src/modals/NewFolderModal.vue +18 -0
- package/src/modals/PreviewModal.vue +7 -2
- package/src/styles/base.css +631 -0
- package/src/types/FileNode.ts +4 -1
- package/dist/index-eARevpz0.js +0 -9952
- package/dist/index-eARevpz0.js.map +0 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SecondaryPane — wiring:d1 split görünümün sağ (ikincil) paneli.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately LIGHT: its own path + its own listing — fetched through
|
|
6
|
+
* the SAME FileApi instance the host already constructed (`api.index`,
|
|
7
|
+
* no copied fetch logic) — a small breadcrumb, double-click navigation,
|
|
8
|
+
* click selection and drag&drop in BOTH directions. No toolbar, no
|
|
9
|
+
* inspector, no upload: those belong to the main panel.
|
|
10
|
+
*
|
|
11
|
+
* Path semantics, confinement (`clamp`) and wire-form resolution
|
|
12
|
+
* (`qualify` / `toUser`) are injected from the host so the pane can
|
|
13
|
+
* never drift from the main panel's multi-storage / rootFloor rules.
|
|
14
|
+
*
|
|
15
|
+
* Cross-pane transfers only EMIT (`transfer`); the host decides
|
|
16
|
+
* move-vs-copy and runs the existing ops APIs. Unscoped styles
|
|
17
|
+
* (`fe-split*` in base.css) — webcomponent data-v rule.
|
|
18
|
+
*/
|
|
19
|
+
import { computed, ref } from 'vue';
|
|
20
|
+
import type { FileApi } from '../composables/useFileApi';
|
|
21
|
+
import type { FileNode } from '../types/FileNode';
|
|
22
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
23
|
+
import { useLocale } from '../composables/useLocale';
|
|
24
|
+
import { fileIconSvg } from '../lib/fileIcons';
|
|
25
|
+
|
|
26
|
+
// Same literals ListView/GridView already hardcode for the internal DnD
|
|
27
|
+
// channel; `-src` carries the origin directory so the host can move (and
|
|
28
|
+
// undo) correctly across panes.
|
|
29
|
+
const FE_DND_MIME = 'application/x-brf-files';
|
|
30
|
+
const FE_DND_SRC_MIME = 'application/x-brf-files-src';
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
api: FileApi;
|
|
34
|
+
/** Opening location, in the host's `currentPath` form. */
|
|
35
|
+
initialPath: string;
|
|
36
|
+
locale: LocaleCode;
|
|
37
|
+
/** Host's own qualify(): user path → wire `<adapter>://<rel>`. */
|
|
38
|
+
qualify: (p: string) => string;
|
|
39
|
+
/** Host's wire → user-path converter (multi-storage aware). */
|
|
40
|
+
toUser: (wire: string) => string;
|
|
41
|
+
/** Host's rootFloor clamp — keeps the pane inside the confine. */
|
|
42
|
+
clamp: (p: string) => string;
|
|
43
|
+
/** Label for the root crumb (adapter name / storage list). */
|
|
44
|
+
rootLabel: string;
|
|
45
|
+
/** rootFloor (user-path form, '' when unconfined) — trims crumbs. */
|
|
46
|
+
floor?: string;
|
|
47
|
+
/** Multi-storage mode: '' is the virtual drives root (no fetch). */
|
|
48
|
+
multiRoot?: boolean;
|
|
49
|
+
/** Synthesized storage rows for the virtual drives root. */
|
|
50
|
+
virtualRows?: () => FileNode[];
|
|
51
|
+
/** Active-panel highlight (keyboard target). */
|
|
52
|
+
active?: boolean;
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
(e: 'navigate', path: string): void;
|
|
57
|
+
(e: 'activate'): void;
|
|
58
|
+
(e: 'close'): void;
|
|
59
|
+
(e: 'open-tab', path: string): void;
|
|
60
|
+
(e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
|
|
61
|
+
}>();
|
|
62
|
+
|
|
63
|
+
const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
|
|
64
|
+
|
|
65
|
+
const path = ref<string>('');
|
|
66
|
+
const files = ref<FileNode[]>([]);
|
|
67
|
+
const loading = ref(false);
|
|
68
|
+
const error = ref('');
|
|
69
|
+
const selected = ref(new Set<string>());
|
|
70
|
+
|
|
71
|
+
function isStorageRow(n: FileNode): boolean {
|
|
72
|
+
return n.mime_type === 'inode/storage';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const atVirtualRoot = computed(
|
|
76
|
+
() => !!props.multiRoot && !(path.value ?? '').replace(/^\/+|\/+$/g, ''),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
async function loadPane(target?: string): Promise<void> {
|
|
80
|
+
const requested = props.clamp(target ?? path.value ?? '');
|
|
81
|
+
loading.value = true;
|
|
82
|
+
error.value = '';
|
|
83
|
+
try {
|
|
84
|
+
// Multi-storage virtual root: synthesize the drives list, no backend
|
|
85
|
+
// call — mirrors the main panel's load() branch.
|
|
86
|
+
if (props.multiRoot && !requested) {
|
|
87
|
+
files.value = props.virtualRows ? props.virtualRows() : [];
|
|
88
|
+
path.value = '';
|
|
89
|
+
selected.value = new Set();
|
|
90
|
+
emit('navigate', '');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const resp = await props.api.index(props.qualify(requested));
|
|
94
|
+
// Same internal-entry filter as the main listing.
|
|
95
|
+
files.value = (resp.files || []).filter((f) => {
|
|
96
|
+
if (f.path.includes('.thumbs')) return false;
|
|
97
|
+
if (f.path.includes('.versions') || f.basename === '.versions') return false;
|
|
98
|
+
if (f.basename === '.trash') return false;
|
|
99
|
+
if (f.basename === '.keepdir') return false;
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
path.value = props.toUser(resp.dirname);
|
|
103
|
+
selected.value = new Set();
|
|
104
|
+
emit('navigate', path.value);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
107
|
+
} finally {
|
|
108
|
+
loading.value = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void loadPane(props.initialPath ?? '');
|
|
113
|
+
|
|
114
|
+
// ------------------------------------------------------------------
|
|
115
|
+
// Breadcrumb (kırıntı)
|
|
116
|
+
// ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
interface Crumb {
|
|
119
|
+
label: string;
|
|
120
|
+
target: string;
|
|
121
|
+
last: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const crumbs = computed<Crumb[]>(() => {
|
|
125
|
+
const clean = (path.value || '').replace(/^\/+|\/+$/g, '');
|
|
126
|
+
const segs = clean ? clean.split('/') : [];
|
|
127
|
+
const floor = (props.floor || '').replace(/^\/+|\/+$/g, '');
|
|
128
|
+
const out: Crumb[] = [];
|
|
129
|
+
let acc = '';
|
|
130
|
+
for (const s of segs) {
|
|
131
|
+
acc = acc ? `${acc}/${s}` : s;
|
|
132
|
+
// Confined embeds: hide the segments strictly ABOVE the floor — the
|
|
133
|
+
// floor segment itself acts as the pane's root crumb.
|
|
134
|
+
if (floor && !(acc === floor || acc.startsWith(floor + '/'))) continue;
|
|
135
|
+
out.push({ label: s, target: acc, last: false });
|
|
136
|
+
}
|
|
137
|
+
if (out.length > 0) out[out.length - 1].last = true;
|
|
138
|
+
return out;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const showRootCrumb = computed(() => !props.floor);
|
|
142
|
+
|
|
143
|
+
function crumbGo(target: string) {
|
|
144
|
+
emit('activate');
|
|
145
|
+
void loadPane(target);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ------------------------------------------------------------------
|
|
149
|
+
// Selection + navigation
|
|
150
|
+
// ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function onRowClick(n: FileNode, ev: MouseEvent) {
|
|
153
|
+
emit('activate');
|
|
154
|
+
const multi = ev.ctrlKey || ev.metaKey;
|
|
155
|
+
const next = new Set<string>(multi ? selected.value : []);
|
|
156
|
+
if (multi && next.has(n.path)) next.delete(n.path);
|
|
157
|
+
else next.add(n.path);
|
|
158
|
+
selected.value = next;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onRowDbl(n: FileNode) {
|
|
162
|
+
if (n.type !== 'dir') return;
|
|
163
|
+
void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Middle-button mousedown must be cancelled on rows: in a scrollable body
|
|
167
|
+
// Chromium's autoscroll takes over and auxclick is never generated.
|
|
168
|
+
function onRowMiddleDown(ev: MouseEvent) {
|
|
169
|
+
if (ev.button === 1) ev.preventDefault();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Middle-click on a folder opens it in a NEW TAB (same convention as the
|
|
173
|
+
// main listing). stopPropagation keeps the host's delegated auxclick
|
|
174
|
+
// listener from double-handling it.
|
|
175
|
+
function onRowAux(n: FileNode, ev: MouseEvent) {
|
|
176
|
+
if (ev.button !== 1) return;
|
|
177
|
+
ev.preventDefault();
|
|
178
|
+
ev.stopPropagation();
|
|
179
|
+
if (n.type !== 'dir') return;
|
|
180
|
+
emit('open-tab', isStorageRow(n) ? n.path : props.toUser(n.path));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function clearSelection() {
|
|
184
|
+
selected.value = new Set();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ------------------------------------------------------------------
|
|
188
|
+
// Drag source
|
|
189
|
+
// ------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function onRowDragStart(n: FileNode, ev: DragEvent) {
|
|
192
|
+
if (!ev.dataTransfer) return;
|
|
193
|
+
if (isStorageRow(n) || atVirtualRoot.value) {
|
|
194
|
+
ev.preventDefault();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
|
|
198
|
+
const items = files.value
|
|
199
|
+
.filter((f) => selected.value.has(f.path) && !isStorageRow(f))
|
|
200
|
+
.map((f) => ({ path: f.path, basename: f.basename, type: f.type }));
|
|
201
|
+
if (items.length === 0) {
|
|
202
|
+
ev.preventDefault();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
|
|
206
|
+
ev.dataTransfer.setData(FE_DND_SRC_MIME, props.qualify(path.value));
|
|
207
|
+
ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
|
|
208
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ------------------------------------------------------------------
|
|
212
|
+
// Drop target (dir rows + pane background)
|
|
213
|
+
// ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
const dropPath = ref<string | null>(null);
|
|
216
|
+
const dropBg = ref(false);
|
|
217
|
+
|
|
218
|
+
function acceptDrag(ev: DragEvent): boolean {
|
|
219
|
+
return !!ev.dataTransfer?.types.includes(FE_DND_MIME);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function handleDropPayload(ev: DragEvent, targetWire: string) {
|
|
223
|
+
const raw = ev.dataTransfer?.getData(FE_DND_MIME);
|
|
224
|
+
if (!raw || !targetWire) return;
|
|
225
|
+
let items: Array<{ path: string }> = [];
|
|
226
|
+
try {
|
|
227
|
+
items = JSON.parse(raw);
|
|
228
|
+
} catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const origin = ev.dataTransfer?.getData(FE_DND_SRC_MIME) || undefined;
|
|
232
|
+
const sources = items
|
|
233
|
+
.map((i) => i.path)
|
|
234
|
+
.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
|
|
235
|
+
if (sources.length === 0) return;
|
|
236
|
+
ev.preventDefault();
|
|
237
|
+
ev.stopPropagation();
|
|
238
|
+
emit('transfer', { sources, targetWire, originWire: origin });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onRowDragOver(n: FileNode, ev: DragEvent) {
|
|
242
|
+
if (n.type !== 'dir' || isStorageRow(n)) return;
|
|
243
|
+
if (!acceptDrag(ev)) return;
|
|
244
|
+
ev.preventDefault();
|
|
245
|
+
ev.stopPropagation();
|
|
246
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
247
|
+
dropPath.value = n.path;
|
|
248
|
+
dropBg.value = false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function onRowDragLeave(n: FileNode) {
|
|
252
|
+
if (dropPath.value === n.path) dropPath.value = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function onRowDrop(n: FileNode, ev: DragEvent) {
|
|
256
|
+
dropPath.value = null;
|
|
257
|
+
dropBg.value = false;
|
|
258
|
+
if (n.type !== 'dir' || isStorageRow(n)) return;
|
|
259
|
+
if (!acceptDrag(ev)) return;
|
|
260
|
+
handleDropPayload(ev, n.path); // real rows are wire-qualified
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onBgDragOver(ev: DragEvent) {
|
|
264
|
+
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
265
|
+
ev.preventDefault();
|
|
266
|
+
ev.stopPropagation();
|
|
267
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
268
|
+
dropBg.value = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function onBgDragLeave() {
|
|
272
|
+
dropBg.value = false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function onBgDrop(ev: DragEvent) {
|
|
276
|
+
dropBg.value = false;
|
|
277
|
+
dropPath.value = null;
|
|
278
|
+
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
279
|
+
handleDropPayload(ev, props.qualify(path.value));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ------------------------------------------------------------------
|
|
283
|
+
// Host API (keyboard routing when this pane is active)
|
|
284
|
+
// ------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function goUp() {
|
|
287
|
+
const cur = (path.value ?? '').replace(/^\/+|\/+$/g, '');
|
|
288
|
+
const floor = (props.floor || '').replace(/^\/+|\/+$/g, '');
|
|
289
|
+
if (!cur || cur === floor) return;
|
|
290
|
+
const idx = cur.lastIndexOf('/');
|
|
291
|
+
void loadPane(idx === -1 ? '' : cur.slice(0, idx));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function selectAll() {
|
|
295
|
+
selected.value = new Set(files.value.filter((f) => !isStorageRow(f)).map((f) => f.path));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function selectedNodes(): FileNode[] {
|
|
299
|
+
return files.value.filter((f) => selected.value.has(f.path));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function openSelected() {
|
|
303
|
+
const n = selectedNodes()[0];
|
|
304
|
+
if (n && n.type === 'dir') onRowDbl(n);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function reload(): Promise<void> {
|
|
308
|
+
return loadPane();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getPath(): string {
|
|
312
|
+
return path.value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
316
|
+
|
|
317
|
+
function specialEmojiFor(n: FileNode): string | null {
|
|
318
|
+
return isStorageRow(n) ? '💾' : null;
|
|
319
|
+
}
|
|
320
|
+
</script>
|
|
321
|
+
|
|
322
|
+
<template>
|
|
323
|
+
<section
|
|
324
|
+
class="fe-split"
|
|
325
|
+
:class="{ 'fe-pane--focus': active }"
|
|
326
|
+
role="region"
|
|
327
|
+
:aria-label="t('split.pane')"
|
|
328
|
+
@pointerdown="emit('activate')"
|
|
329
|
+
>
|
|
330
|
+
<header class="fe-split__crumbs">
|
|
331
|
+
<button
|
|
332
|
+
v-if="showRootCrumb"
|
|
333
|
+
type="button"
|
|
334
|
+
class="fe-split__crumb"
|
|
335
|
+
:class="{ 'is-last': crumbs.length === 0 }"
|
|
336
|
+
:title="rootLabel"
|
|
337
|
+
@click="crumbGo('')"
|
|
338
|
+
>{{ rootLabel }}</button>
|
|
339
|
+
<template v-for="c in crumbs" :key="c.target">
|
|
340
|
+
<span class="fe-split__sep" aria-hidden="true">›</span>
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
class="fe-split__crumb"
|
|
344
|
+
:class="{ 'is-last': c.last }"
|
|
345
|
+
:title="c.label"
|
|
346
|
+
@click="crumbGo(c.target)"
|
|
347
|
+
>{{ c.label }}</button>
|
|
348
|
+
</template>
|
|
349
|
+
<span class="fe-split__spacer"></span>
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
class="fe-split__closebtn"
|
|
353
|
+
:aria-label="t('split.close')"
|
|
354
|
+
:title="t('split.close')"
|
|
355
|
+
@click="emit('close')"
|
|
356
|
+
>×</button>
|
|
357
|
+
</header>
|
|
358
|
+
|
|
359
|
+
<div
|
|
360
|
+
class="fe-split__body"
|
|
361
|
+
:class="{ 'is-dropover': dropBg }"
|
|
362
|
+
@click.self="clearSelection"
|
|
363
|
+
@dragover="onBgDragOver"
|
|
364
|
+
@dragleave="onBgDragLeave"
|
|
365
|
+
@drop="onBgDrop"
|
|
366
|
+
>
|
|
367
|
+
<div v-if="loading && files.length === 0" class="fe-split__state" role="status">
|
|
368
|
+
{{ t('loading') }}
|
|
369
|
+
</div>
|
|
370
|
+
<div v-else-if="error" class="fe-split__state">
|
|
371
|
+
<span>{{ t('split.error') }}</span>
|
|
372
|
+
<button type="button" class="fe-btn" @click="() => loadPane()">
|
|
373
|
+
{{ t('split.retry') }}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div v-else-if="files.length === 0" class="fe-split__state">
|
|
377
|
+
{{ t('empty.folder') }}
|
|
378
|
+
</div>
|
|
379
|
+
<div v-else class="fe-split__list" role="listbox" aria-multiselectable="true">
|
|
380
|
+
<div
|
|
381
|
+
v-for="n in files"
|
|
382
|
+
:key="n.path"
|
|
383
|
+
class="fe-split__row"
|
|
384
|
+
:class="{
|
|
385
|
+
'is-selected': selected.has(n.path),
|
|
386
|
+
'is-dir': n.type === 'dir',
|
|
387
|
+
'is-droptarget': dropPath === n.path,
|
|
388
|
+
}"
|
|
389
|
+
role="option"
|
|
390
|
+
:aria-selected="selected.has(n.path) ? 'true' : 'false'"
|
|
391
|
+
tabindex="0"
|
|
392
|
+
draggable="true"
|
|
393
|
+
@click="onRowClick(n, $event)"
|
|
394
|
+
@dblclick="onRowDbl(n)"
|
|
395
|
+
@mousedown="onRowMiddleDown($event)"
|
|
396
|
+
@auxclick="onRowAux(n, $event)"
|
|
397
|
+
@dragstart="onRowDragStart(n, $event)"
|
|
398
|
+
@dragover="onRowDragOver(n, $event)"
|
|
399
|
+
@dragleave="onRowDragLeave(n)"
|
|
400
|
+
@drop="onRowDrop(n, $event)"
|
|
401
|
+
>
|
|
402
|
+
<span v-if="specialEmojiFor(n)" class="fe-split__icon" aria-hidden="true">{{ specialEmojiFor(n) }}</span>
|
|
403
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
404
|
+
<span v-else class="fe-split__icon" aria-hidden="true" v-html="fileIconSvg(n)"></span>
|
|
405
|
+
<span class="fe-split__name" :title="n.basename">{{ nodeDisplayName(n) }}</span>
|
|
406
|
+
<span class="fe-split__size">{{ n.type === 'dir' ? '' : formatSize(n.size) }}</span>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</section>
|
|
411
|
+
</template>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TabBar — wiring:d1 sekme şeridi.
|
|
4
|
+
*
|
|
5
|
+
* Thin strip that sits ABOVE the toolbar. The host renders it only when
|
|
6
|
+
* 2+ tabs exist (embed pixel-parity: a single tab shows nothing at all).
|
|
7
|
+
* Purely presentational: every mutation is an emit; useTabs owns state.
|
|
8
|
+
*
|
|
9
|
+
* Interactions:
|
|
10
|
+
* click tab → select
|
|
11
|
+
* × / middle-tık → close
|
|
12
|
+
* + → new tab (clone of the current location)
|
|
13
|
+
* drag → reorder (HTML5 DnD; live-swap while dragging)
|
|
14
|
+
* ⫿ (split) → toggle the active tab's secondary pane
|
|
15
|
+
*
|
|
16
|
+
* UNSCOPED styles (`fe-tabs*` in base.css) — the webcomponent build
|
|
17
|
+
* cannot carry scoped data-v hashes (v0.5.0 discovery).
|
|
18
|
+
*/
|
|
19
|
+
import { ref } from 'vue';
|
|
20
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
21
|
+
import { useLocale } from '../composables/useLocale';
|
|
22
|
+
|
|
23
|
+
export interface TabItem {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
/** true → the tab carries a split pane (small accent dot). */
|
|
27
|
+
split: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
tabs: TabItem[];
|
|
32
|
+
activeId: string;
|
|
33
|
+
locale: LocaleCode;
|
|
34
|
+
/** false → the split toggle is hidden (narrow embeds). */
|
|
35
|
+
splitEnabled?: boolean;
|
|
36
|
+
/** true → the split toggle renders pressed (active tab has a pane). */
|
|
37
|
+
splitActive?: boolean;
|
|
38
|
+
}>();
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits<{
|
|
41
|
+
(e: 'select', id: string): void;
|
|
42
|
+
(e: 'close', id: string): void;
|
|
43
|
+
(e: 'new'): void;
|
|
44
|
+
(e: 'reorder', from: number, to: number): void;
|
|
45
|
+
(e: 'toggle-split'): void;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
const { t } = useLocale(() => props.locale);
|
|
49
|
+
|
|
50
|
+
// ---- drag-sort (HTML5 DnD, live swap) ------------------------------
|
|
51
|
+
const dragIndex = ref<number | null>(null);
|
|
52
|
+
const overIndex = ref<number | null>(null);
|
|
53
|
+
|
|
54
|
+
function onDragStart(i: number, ev: DragEvent) {
|
|
55
|
+
dragIndex.value = i;
|
|
56
|
+
if (ev.dataTransfer) {
|
|
57
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
58
|
+
// Firefox requires SOME payload for a drag to start. Deliberately not
|
|
59
|
+
// the internal file-DnD MIME, so the explorer's drop surfaces ignore it.
|
|
60
|
+
ev.dataTransfer.setData('text/plain', props.tabs[i]?.id ?? '');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onDragOver(i: number, ev: DragEvent) {
|
|
65
|
+
if (dragIndex.value === null) return;
|
|
66
|
+
ev.preventDefault();
|
|
67
|
+
ev.stopPropagation();
|
|
68
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
69
|
+
overIndex.value = i;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onDragLeave(i: number) {
|
|
73
|
+
if (overIndex.value === i) overIndex.value = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onDrop(i: number, ev: DragEvent) {
|
|
77
|
+
if (dragIndex.value === null) return;
|
|
78
|
+
ev.preventDefault();
|
|
79
|
+
ev.stopPropagation();
|
|
80
|
+
if (dragIndex.value !== i) emit('reorder', dragIndex.value, i);
|
|
81
|
+
dragIndex.value = null;
|
|
82
|
+
overIndex.value = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onDragEnd() {
|
|
86
|
+
dragIndex.value = null;
|
|
87
|
+
overIndex.value = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Middle-click closes a tab (browser convention). The mousedown guard
|
|
91
|
+
// cancels Chromium's autoscroll on the scrollable strip — without it the
|
|
92
|
+
// auxclick event is never generated.
|
|
93
|
+
function onTabMiddleDown(ev: MouseEvent) {
|
|
94
|
+
if (ev.button === 1) ev.preventDefault();
|
|
95
|
+
}
|
|
96
|
+
function onAux(id: string, ev: MouseEvent) {
|
|
97
|
+
if (ev.button !== 1) return;
|
|
98
|
+
ev.preventDefault();
|
|
99
|
+
ev.stopPropagation();
|
|
100
|
+
emit('close', id);
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<div class="fe-tabs" role="tablist" :aria-label="t('tabs.strip')">
|
|
106
|
+
<div class="fe-tabs__scroll">
|
|
107
|
+
<div
|
|
108
|
+
v-for="(tab, i) in tabs"
|
|
109
|
+
:key="tab.id"
|
|
110
|
+
class="fe-tabs__tab"
|
|
111
|
+
:class="{
|
|
112
|
+
'is-active': tab.id === activeId,
|
|
113
|
+
'is-dragover': overIndex === i && dragIndex !== null && dragIndex !== i,
|
|
114
|
+
}"
|
|
115
|
+
role="tab"
|
|
116
|
+
:aria-selected="tab.id === activeId ? 'true' : 'false'"
|
|
117
|
+
tabindex="0"
|
|
118
|
+
draggable="true"
|
|
119
|
+
:title="tab.label"
|
|
120
|
+
@click="emit('select', tab.id)"
|
|
121
|
+
@keydown.enter.prevent="emit('select', tab.id)"
|
|
122
|
+
@mousedown="onTabMiddleDown($event)"
|
|
123
|
+
@auxclick="onAux(tab.id, $event)"
|
|
124
|
+
@dragstart="onDragStart(i, $event)"
|
|
125
|
+
@dragover="onDragOver(i, $event)"
|
|
126
|
+
@dragleave="onDragLeave(i)"
|
|
127
|
+
@drop="onDrop(i, $event)"
|
|
128
|
+
@dragend="onDragEnd"
|
|
129
|
+
>
|
|
130
|
+
<span v-if="tab.split" class="fe-tabs__splitdot" aria-hidden="true"></span>
|
|
131
|
+
<span class="fe-tabs__label">{{ tab.label }}</span>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="fe-tabs__close"
|
|
135
|
+
:aria-label="t('tabs.close')"
|
|
136
|
+
:title="t('tabs.close')"
|
|
137
|
+
@click.stop="emit('close', tab.id)"
|
|
138
|
+
>×</button>
|
|
139
|
+
</div>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="fe-tabs__new"
|
|
143
|
+
:aria-label="t('tabs.new')"
|
|
144
|
+
:title="t('tabs.new')"
|
|
145
|
+
@click="emit('new')"
|
|
146
|
+
>+</button>
|
|
147
|
+
</div>
|
|
148
|
+
<button
|
|
149
|
+
v-if="splitEnabled !== false"
|
|
150
|
+
type="button"
|
|
151
|
+
class="fe-tabs__split"
|
|
152
|
+
:class="{ 'is-active': splitActive }"
|
|
153
|
+
:aria-label="splitActive ? t('tabs.split_off') : t('tabs.split')"
|
|
154
|
+
:title="splitActive ? t('tabs.split_off') : t('tabs.split')"
|
|
155
|
+
:aria-pressed="splitActive ? 'true' : 'false'"
|
|
156
|
+
@click="emit('toggle-split')"
|
|
157
|
+
>
|
|
158
|
+
<svg
|
|
159
|
+
class="fe-ficon"
|
|
160
|
+
viewBox="0 0 24 24"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke="currentColor"
|
|
163
|
+
stroke-width="1.8"
|
|
164
|
+
stroke-linecap="round"
|
|
165
|
+
stroke-linejoin="round"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
focusable="false"
|
|
168
|
+
>
|
|
169
|
+
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
170
|
+
<path d="M12 4v16" />
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
@@ -221,11 +221,12 @@ const moreActions = computed<ContextAction[]>(() => {
|
|
|
221
221
|
: t('toolbar.density.compact'),
|
|
222
222
|
icon: '⇅',
|
|
223
223
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
);
|
|
224
|
+
/* wiring:d2 — dar mod ⋯ menüsü: aktif olmayan İKİ görünüm de listelenir
|
|
225
|
+
(list/grid/gallery); eski tekli toggle üç modda eksik kalıyordu. */
|
|
226
|
+
if (props.viewMode !== 'list') list.push({ key: 'view-list', label: t('toolbar.view.list'), icon: '☰' });
|
|
227
|
+
if (props.viewMode !== 'grid') list.push({ key: 'view-grid', label: t('toolbar.view.grid'), icon: '▦' });
|
|
228
|
+
if (props.viewMode !== 'gallery') list.push({ key: 'view-gallery', label: t('toolbar.view.gallery'), icon: '▣' });
|
|
229
|
+
/* /wiring:d2 */
|
|
229
230
|
/* koru:k1 — inspector toggle also reachable from the narrow overflow menu */
|
|
230
231
|
list.push({ key: 'inspector', label: t('toolbar.inspector'), icon: 'ℹ' });
|
|
231
232
|
/* wiring:c1 — tema galerisi de dar modda ⋯ menüsünden açılır */
|
|
@@ -257,6 +258,9 @@ function onMoreSelect(a: ContextAction) {
|
|
|
257
258
|
case 'view-grid':
|
|
258
259
|
emit('update:viewMode', 'grid');
|
|
259
260
|
break;
|
|
261
|
+
case 'view-gallery' /* wiring:d2 */:
|
|
262
|
+
emit('update:viewMode', 'gallery');
|
|
263
|
+
break;
|
|
260
264
|
case 'inspector' /* koru:k1 */:
|
|
261
265
|
emit('toggle-inspector');
|
|
262
266
|
break;
|
|
@@ -487,6 +491,20 @@ function onMoreSelect(a: ContextAction) {
|
|
|
487
491
|
>
|
|
488
492
|
<span class="fe-icon" aria-hidden="true">▦</span>
|
|
489
493
|
</button>
|
|
494
|
+
<!-- wiring:d2 — üçüncü görünüm: galeri -->
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
class="fe-btn fe-btn--icon-only"
|
|
498
|
+
:class="{ 'is-active': viewMode === 'gallery' }"
|
|
499
|
+
role="tab"
|
|
500
|
+
:aria-selected="viewMode === 'gallery'"
|
|
501
|
+
:title="t('toolbar.view.gallery')"
|
|
502
|
+
:aria-label="t('toolbar.view.gallery')"
|
|
503
|
+
@click="emit('update:viewMode', 'gallery')"
|
|
504
|
+
>
|
|
505
|
+
<span class="fe-icon" aria-hidden="true">▣</span>
|
|
506
|
+
</button>
|
|
507
|
+
<!-- /wiring:d2 -->
|
|
490
508
|
</div>
|
|
491
509
|
</template>
|
|
492
510
|
|