@brftech/filex-core 0.6.0 → 0.7.1
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-EGgNqLPE.js → ArchiveViewer-Qi4L2jj9.js} +2 -2
- package/dist/{ArchiveViewer-EGgNqLPE.js.map → ArchiveViewer-Qi4L2jj9.js.map} +1 -1
- package/dist/{CsvViewer-BvriJ42M.js → CsvViewer-IOIrJDsJ.js} +2 -2
- package/dist/{CsvViewer-BvriJ42M.js.map → CsvViewer-IOIrJDsJ.js.map} +1 -1
- package/dist/{DrawioViewer-B_bSPsWX.js → DrawioViewer-D0KtUQBO.js} +2 -2
- package/dist/{DrawioViewer-B_bSPsWX.js.map → DrawioViewer-D0KtUQBO.js.map} +1 -1
- package/dist/{EpubViewer-hl68h4gG.js → EpubViewer-DJOXiOi4.js} +2 -2
- package/dist/{EpubViewer-hl68h4gG.js.map → EpubViewer-DJOXiOi4.js.map} +1 -1
- package/dist/{IpynbViewer-lhwOjXK7.js → IpynbViewer-D16jXVJc.js} +2 -2
- package/dist/{IpynbViewer-lhwOjXK7.js.map → IpynbViewer-D16jXVJc.js.map} +1 -1
- package/dist/{MermaidViewer-H2Z6AYXg.js → MermaidViewer-BnSaVg1L.js} +2 -2
- package/dist/{MermaidViewer-H2Z6AYXg.js.map → MermaidViewer-BnSaVg1L.js.map} +1 -1
- package/dist/{PsdViewer-Bsk-11yH.js → PsdViewer-BUerXaQA.js} +2 -2
- package/dist/{PsdViewer-Bsk-11yH.js.map → PsdViewer-BUerXaQA.js.map} +1 -1
- package/dist/{TiffViewer-D-j9vycG.js → TiffViewer-D0Qn3IV-.js} +2 -2
- package/dist/{TiffViewer-D-j9vycG.js.map → TiffViewer-D0Qn3IV-.js.map} +1 -1
- package/dist/{Viewer3D-Bcr70rTe.js → Viewer3D-BtRK-k1B.js} +2 -2
- package/dist/{Viewer3D-Bcr70rTe.js.map → Viewer3D-BtRK-k1B.js.map} +1 -1
- package/dist/filex-core.js +66 -50
- package/dist/filex-core.umd.cjs +35 -35
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-CGNE1bSv.js +11802 -0
- package/dist/index-CGNE1bSv.js.map +1 -0
- package/dist/index.d.ts +184 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +416 -15
- package/src/components/EncryptedFolderModal.vue +125 -0
- package/src/components/GalleryView.vue +1 -0
- package/src/components/GridView.vue +1 -0
- package/src/components/ListView.vue +1 -0
- package/src/components/SecondaryPane.vue +72 -78
- package/src/components/Toolbar.vue +112 -3
- package/src/composables/useFileApi.ts +7 -0
- package/src/composables/useKeyboardShortcuts.ts +6 -0
- package/src/composables/useTabs.ts +11 -2
- package/src/index.ts +21 -0
- package/src/lib/e2ecrypto.ts +299 -0
- package/src/locales/en.ts +34 -0
- package/src/locales/tr.ts +34 -0
- package/src/modals/NewFolderModal.vue +18 -0
- package/src/modals/PreviewModal.vue +7 -2
- package/src/styles/base.css +126 -1
- package/src/styles/variables.css +2 -2
- package/src/types/FileNode.ts +3 -0
- package/dist/index-C_ZSnk_I.js +0 -11109
- package/dist/index-C_ZSnk_I.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EncryptedFolderModal — create an E2E-encrypted folder (wiring:e2).
|
|
4
|
+
*
|
|
5
|
+
* Collects folder name + password ×2 and an explicit "I understand there
|
|
6
|
+
* is NO recovery" acknowledgement. The parent (FileExplorer) performs the
|
|
7
|
+
* actual newfolder + marker-upload dance; this modal never touches the
|
|
8
|
+
* network and never stores the password anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Crypto scheme + threat model: docs/E2E-ENCRYPTION.md.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, watch } from 'vue';
|
|
13
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
14
|
+
import { useLocale } from '../composables/useLocale';
|
|
15
|
+
import { E2E_MIN_PASSWORD_LEN } from '../lib/e2ecrypto';
|
|
16
|
+
import Modal from '../modals/Modal.vue';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
open: boolean;
|
|
20
|
+
locale: LocaleCode;
|
|
21
|
+
/** True while the parent is creating the folder + uploading the marker. */
|
|
22
|
+
busy?: boolean;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
(e: 'close'): void;
|
|
27
|
+
(e: 'submit', payload: { name: string; password: string }): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const { t } = useLocale(() => props.locale);
|
|
31
|
+
const name = ref('');
|
|
32
|
+
const password = ref('');
|
|
33
|
+
const password2 = ref('');
|
|
34
|
+
const ack = ref(false);
|
|
35
|
+
const err = ref<string | null>(null);
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.open,
|
|
39
|
+
(v) => {
|
|
40
|
+
if (v) {
|
|
41
|
+
name.value = '';
|
|
42
|
+
password.value = '';
|
|
43
|
+
password2.value = '';
|
|
44
|
+
ack.value = false;
|
|
45
|
+
err.value = null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
function submit() {
|
|
51
|
+
if (props.busy) return;
|
|
52
|
+
const clean = name.value.trim();
|
|
53
|
+
if (!clean) {
|
|
54
|
+
err.value = t('modal.newfolder.placeholder');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (/[\\/]/.test(clean) || clean === '.' || clean === '..' || clean.startsWith('.filex')) {
|
|
58
|
+
err.value = t('e2e.create.bad_name');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (password.value.length < E2E_MIN_PASSWORD_LEN) {
|
|
62
|
+
err.value = t('e2e.create.pw_short');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (password.value !== password2.value) {
|
|
66
|
+
err.value = t('e2e.create.pw_mismatch');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!ack.value) {
|
|
70
|
+
err.value = t('e2e.create.ack_required');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
err.value = null;
|
|
74
|
+
emit('submit', { name: clean, password: password.value });
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<Modal :open="open" :title="t('e2e.create.title')" size="sm" @close="emit('close')">
|
|
80
|
+
<form class="fe-e2e-form" @submit.prevent="submit">
|
|
81
|
+
<input
|
|
82
|
+
v-model="name"
|
|
83
|
+
type="text"
|
|
84
|
+
class="fe-input"
|
|
85
|
+
:placeholder="t('modal.newfolder.placeholder')"
|
|
86
|
+
autocomplete="off"
|
|
87
|
+
:disabled="busy"
|
|
88
|
+
/>
|
|
89
|
+
<input
|
|
90
|
+
v-model="password"
|
|
91
|
+
type="password"
|
|
92
|
+
class="fe-input"
|
|
93
|
+
:placeholder="t('e2e.create.pw_placeholder')"
|
|
94
|
+
autocomplete="new-password"
|
|
95
|
+
:disabled="busy"
|
|
96
|
+
/>
|
|
97
|
+
<input
|
|
98
|
+
v-model="password2"
|
|
99
|
+
type="password"
|
|
100
|
+
class="fe-input"
|
|
101
|
+
:placeholder="t('e2e.create.pw2_placeholder')"
|
|
102
|
+
autocomplete="new-password"
|
|
103
|
+
:disabled="busy"
|
|
104
|
+
@keydown.enter.prevent="submit"
|
|
105
|
+
/>
|
|
106
|
+
<div class="fe-e2e-warn" role="alert">
|
|
107
|
+
<strong>{{ t('e2e.create.warn_title') }}</strong>
|
|
108
|
+
<p>{{ t('e2e.create.warn_body') }}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<label class="fe-e2e-ack">
|
|
111
|
+
<input v-model="ack" type="checkbox" :disabled="busy" />
|
|
112
|
+
<span>{{ t('e2e.create.ack') }}</span>
|
|
113
|
+
</label>
|
|
114
|
+
<p v-if="err" class="fe-form__error">{{ err }}</p>
|
|
115
|
+
</form>
|
|
116
|
+
<template #actions>
|
|
117
|
+
<button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">
|
|
118
|
+
{{ t('modal.newfolder.cancel') }}
|
|
119
|
+
</button>
|
|
120
|
+
<button type="button" class="fe-btn fe-btn--primary" :disabled="busy" @click="submit">
|
|
121
|
+
{{ busy ? t('e2e.create.busy') : t('e2e.create.create') }}
|
|
122
|
+
</button>
|
|
123
|
+
</template>
|
|
124
|
+
</Modal>
|
|
125
|
+
</template>
|
|
@@ -127,6 +127,7 @@ function parentDir(path: string): string {
|
|
|
127
127
|
function specialEmojiFor(n: FileNode): string | null {
|
|
128
128
|
if (n.basename === '.trash') return '🗑';
|
|
129
129
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
130
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
130
131
|
return null;
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -126,6 +126,7 @@ function parentDir(path: string): string {
|
|
|
126
126
|
function specialEmojiFor(n: FileNode): string | null {
|
|
127
127
|
if (n.basename === '.trash') return '🗑';
|
|
128
128
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
129
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -139,6 +139,7 @@ function cancelPress() {
|
|
|
139
139
|
function specialEmojiFor(n: FileNode): string | null {
|
|
140
140
|
if (n.basename === '.trash') return '🗑';
|
|
141
141
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
142
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
142
143
|
return null;
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { computed, ref } from 'vue';
|
|
20
20
|
import type { FileApi } from '../composables/useFileApi';
|
|
21
|
-
import type { FileNode } from '../types/FileNode';
|
|
21
|
+
import type { FileNode, ViewMode } from '../types/FileNode';
|
|
22
22
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
23
23
|
import { useLocale } from '../composables/useLocale';
|
|
24
|
-
import
|
|
24
|
+
import ListView from './ListView.vue';
|
|
25
|
+
import GridView from './GridView.vue';
|
|
26
|
+
import GalleryView from './GalleryView.vue';
|
|
25
27
|
|
|
26
28
|
// Same literals ListView/GridView already hardcode for the internal DnD
|
|
27
29
|
// channel; `-src` carries the origin directory so the host can move (and
|
|
@@ -50,6 +52,11 @@ const props = defineProps<{
|
|
|
50
52
|
virtualRows?: () => FileNode[];
|
|
51
53
|
/** Active-panel highlight (keyboard target). */
|
|
52
54
|
active?: boolean;
|
|
55
|
+
/** ui-fix — the pane renders the SAME view components as the main
|
|
56
|
+
* panel (list/grid/gallery) instead of its own flat list. */
|
|
57
|
+
viewMode?: ViewMode;
|
|
58
|
+
/** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
|
|
59
|
+
thumbSrc?: (n: FileNode) => string | null;
|
|
53
60
|
}>();
|
|
54
61
|
|
|
55
62
|
const emit = defineEmits<{
|
|
@@ -60,7 +67,7 @@ const emit = defineEmits<{
|
|
|
60
67
|
(e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
|
|
61
68
|
}>();
|
|
62
69
|
|
|
63
|
-
const { t
|
|
70
|
+
const { t } = useLocale(() => props.locale);
|
|
64
71
|
|
|
65
72
|
const path = ref<string>('');
|
|
66
73
|
const files = ref<FileNode[]>([]);
|
|
@@ -149,35 +156,40 @@ function crumbGo(target: string) {
|
|
|
149
156
|
// Selection + navigation
|
|
150
157
|
// ------------------------------------------------------------------
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
/* ui-fix — adapter for the shared view components' click contract
|
|
160
|
+
* ({ctrl, shift} mod object instead of a MouseEvent). Shift behaves as
|
|
161
|
+
* ctrl here: the pane keeps a simple Set, no range anchor. */
|
|
162
|
+
function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean }) {
|
|
153
163
|
emit('activate');
|
|
154
|
-
const multi =
|
|
164
|
+
const multi = mod.ctrl || mod.shift;
|
|
155
165
|
const next = new Set<string>(multi ? selected.value : []);
|
|
156
166
|
if (multi && next.has(n.path)) next.delete(n.path);
|
|
157
167
|
else next.add(n.path);
|
|
158
168
|
selected.value = next;
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
/* ui-fix — right-click on a pane item: activate + select it, no menu
|
|
172
|
+
* (the pane has no item menu; letting the main panel's menu open here
|
|
173
|
+
* would act on the WRONG panel's selection). */
|
|
174
|
+
function onViewContext(n: FileNode, ev: MouseEvent) {
|
|
175
|
+
ev.preventDefault();
|
|
176
|
+
ev.stopPropagation();
|
|
177
|
+
emit('activate');
|
|
178
|
+
if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
|
|
164
179
|
}
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
function
|
|
169
|
-
|
|
181
|
+
/* ui-fix — drop-into from the shared views: same payload path as the
|
|
182
|
+
* old flat rows (dir check + wire-qualified row paths). */
|
|
183
|
+
function onViewDropInto(target: FileNode, ev: DragEvent) {
|
|
184
|
+
dropBg.value = false;
|
|
185
|
+
if (target.type !== 'dir' || isStorageRow(target)) return;
|
|
186
|
+
if (!acceptDrag(ev)) return;
|
|
187
|
+
handleDropPayload(ev, target.path);
|
|
170
188
|
}
|
|
171
189
|
|
|
172
|
-
|
|
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();
|
|
190
|
+
function onRowDbl(n: FileNode) {
|
|
179
191
|
if (n.type !== 'dir') return;
|
|
180
|
-
|
|
192
|
+
void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
function clearSelection() {
|
|
@@ -212,7 +224,6 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
|
|
|
212
224
|
// Drop target (dir rows + pane background)
|
|
213
225
|
// ------------------------------------------------------------------
|
|
214
226
|
|
|
215
|
-
const dropPath = ref<string | null>(null);
|
|
216
227
|
const dropBg = ref(false);
|
|
217
228
|
|
|
218
229
|
function acceptDrag(ev: DragEvent): boolean {
|
|
@@ -238,28 +249,6 @@ function handleDropPayload(ev: DragEvent, targetWire: string) {
|
|
|
238
249
|
emit('transfer', { sources, targetWire, originWire: origin });
|
|
239
250
|
}
|
|
240
251
|
|
|
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
252
|
function onBgDragOver(ev: DragEvent) {
|
|
264
253
|
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
265
254
|
ev.preventDefault();
|
|
@@ -274,7 +263,6 @@ function onBgDragLeave() {
|
|
|
274
263
|
|
|
275
264
|
function onBgDrop(ev: DragEvent) {
|
|
276
265
|
dropBg.value = false;
|
|
277
|
-
dropPath.value = null;
|
|
278
266
|
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
279
267
|
handleDropPayload(ev, props.qualify(path.value));
|
|
280
268
|
}
|
|
@@ -313,10 +301,6 @@ function getPath(): string {
|
|
|
313
301
|
}
|
|
314
302
|
|
|
315
303
|
defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
316
|
-
|
|
317
|
-
function specialEmojiFor(n: FileNode): string | null {
|
|
318
|
-
return isStorageRow(n) ? '💾' : null;
|
|
319
|
-
}
|
|
320
304
|
</script>
|
|
321
305
|
|
|
322
306
|
<template>
|
|
@@ -376,36 +360,46 @@ function specialEmojiFor(n: FileNode): string | null {
|
|
|
376
360
|
<div v-else-if="files.length === 0" class="fe-split__state">
|
|
377
361
|
{{ t('empty.folder') }}
|
|
378
362
|
</div>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
363
|
+
<!-- ui-fix — aynı görünüm bileşenleri (list/grid/gallery), pane'in
|
|
364
|
+
kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
|
|
365
|
+
<ListView
|
|
366
|
+
v-else-if="(viewMode ?? 'list') === 'list'"
|
|
367
|
+
:files="files"
|
|
368
|
+
:selected="selected"
|
|
369
|
+
:locale="locale"
|
|
370
|
+
:loading="loading"
|
|
371
|
+
@click-row="onViewClick"
|
|
372
|
+
@dbl-row="onRowDbl"
|
|
373
|
+
@context-row="onViewContext"
|
|
374
|
+
@item-drag-start="onRowDragStart"
|
|
375
|
+
@item-drop-into="onViewDropInto"
|
|
376
|
+
/>
|
|
377
|
+
<GridView
|
|
378
|
+
v-else-if="viewMode === 'grid'"
|
|
379
|
+
:files="files"
|
|
380
|
+
:selected="selected"
|
|
381
|
+
:locale="locale"
|
|
382
|
+
:loading="loading"
|
|
383
|
+
:thumb-src="thumbSrc"
|
|
384
|
+
@click-card="onViewClick"
|
|
385
|
+
@dbl-card="onRowDbl"
|
|
386
|
+
@context-card="onViewContext"
|
|
387
|
+
@item-drag-start="onRowDragStart"
|
|
388
|
+
@item-drop-into="onViewDropInto"
|
|
389
|
+
/>
|
|
390
|
+
<GalleryView
|
|
391
|
+
v-else
|
|
392
|
+
:files="files"
|
|
393
|
+
:selected="selected"
|
|
394
|
+
:locale="locale"
|
|
395
|
+
:loading="loading"
|
|
396
|
+
:thumb-src="thumbSrc"
|
|
397
|
+
@click-card="onViewClick"
|
|
398
|
+
@dbl-card="onRowDbl"
|
|
399
|
+
@context-card="onViewContext"
|
|
400
|
+
@item-drag-start="onRowDragStart"
|
|
401
|
+
@item-drop-into="onViewDropInto"
|
|
402
|
+
/>
|
|
409
403
|
</div>
|
|
410
404
|
</section>
|
|
411
405
|
</template>
|
|
@@ -152,6 +152,74 @@ const mode = computed<SelectionMode>(() => props.selectionMode ?? 'none');
|
|
|
152
152
|
// context menu uses, the two menus are guaranteed to match.
|
|
153
153
|
const toolbarItems = computed(() => props.actions.filter((a) => !a.divider && !a.hidden));
|
|
154
154
|
|
|
155
|
+
/* === ui-fix — wide-mode action folding ===============================
|
|
156
|
+
* Long selection-action rows used to wrap the whole toolbar and push the
|
|
157
|
+
* search / view controls onto a second line. The row is now single-line:
|
|
158
|
+
* actions that do not fit fold into a "⋯" menu. Widths come from a
|
|
159
|
+
* hidden measurement strip (all actions always rendered there), so the
|
|
160
|
+
* calculation never mutates what it measures. */
|
|
161
|
+
const primaryEl = ref<HTMLElement | null>(null);
|
|
162
|
+
const measureEl = ref<HTMLElement | null>(null);
|
|
163
|
+
const wideMoreBtnEl = ref<HTMLElement | null>(null);
|
|
164
|
+
const wideMoreRef = ref<InstanceType<typeof ContextMenu> | null>(null);
|
|
165
|
+
const visibleActionCount = ref(Number.MAX_SAFE_INTEGER);
|
|
166
|
+
const visibleToolbarItems = computed(() => toolbarItems.value.slice(0, visibleActionCount.value));
|
|
167
|
+
const overflowToolbarItems = computed(() => toolbarItems.value.slice(visibleActionCount.value));
|
|
168
|
+
const WIDE_MORE_BTN_W = 40;
|
|
169
|
+
|
|
170
|
+
function recalcFold() {
|
|
171
|
+
if (props.narrow) return;
|
|
172
|
+
const cont = primaryEl.value;
|
|
173
|
+
const meas = measureEl.value;
|
|
174
|
+
if (!cont || !meas) {
|
|
175
|
+
visibleActionCount.value = toolbarItems.value.length;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const gap = parseFloat(getComputedStyle(cont).gap) || 8;
|
|
179
|
+
let fixed = 0;
|
|
180
|
+
for (const child of Array.from(cont.children) as HTMLElement[]) {
|
|
181
|
+
if (child === meas || child === wideMoreBtnEl.value) continue;
|
|
182
|
+
if (child.classList.contains('fe-btn--fold')) continue;
|
|
183
|
+
if (child.offsetWidth > 0) fixed += child.offsetWidth + gap;
|
|
184
|
+
}
|
|
185
|
+
const widths = (Array.from(meas.children) as HTMLElement[]).map((c) => c.offsetWidth + gap);
|
|
186
|
+
const total = widths.reduce((s, w) => s + w, 0);
|
|
187
|
+
const avail = cont.clientWidth - fixed;
|
|
188
|
+
if (total <= avail) {
|
|
189
|
+
visibleActionCount.value = widths.length;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let used = WIDE_MORE_BTN_W + gap;
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const w of widths) {
|
|
195
|
+
if (used + w > avail) break;
|
|
196
|
+
used += w;
|
|
197
|
+
count += 1;
|
|
198
|
+
}
|
|
199
|
+
visibleActionCount.value = count;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let foldRo: ResizeObserver | undefined;
|
|
203
|
+
onMounted(() => {
|
|
204
|
+
if (typeof ResizeObserver !== 'undefined' && primaryEl.value) {
|
|
205
|
+
foldRo = new ResizeObserver(() => recalcFold());
|
|
206
|
+
foldRo.observe(primaryEl.value);
|
|
207
|
+
}
|
|
208
|
+
void nextTick(recalcFold);
|
|
209
|
+
});
|
|
210
|
+
onBeforeUnmount(() => foldRo?.disconnect());
|
|
211
|
+
watch(
|
|
212
|
+
() => [toolbarItems.value, props.narrow, props.trashActive, mode.value] as const,
|
|
213
|
+
() => void nextTick(recalcFold),
|
|
214
|
+
{ deep: false },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
function openWideMore() {
|
|
218
|
+
const r = wideMoreBtnEl.value?.getBoundingClientRect();
|
|
219
|
+
wideMoreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
|
|
220
|
+
}
|
|
221
|
+
/* === /ui-fix ========================================================= */
|
|
222
|
+
|
|
155
223
|
function fire(key: string) {
|
|
156
224
|
emit('action', key);
|
|
157
225
|
}
|
|
@@ -291,7 +359,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
291
359
|
>
|
|
292
360
|
<!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
|
|
293
361
|
<template v-if="!narrow">
|
|
294
|
-
<div class="fe-toolbar__primary">
|
|
362
|
+
<div ref="primaryEl" class="fe-toolbar__primary">
|
|
295
363
|
<button
|
|
296
364
|
v-if="canGoUp"
|
|
297
365
|
type="button"
|
|
@@ -315,10 +383,10 @@ function onMoreSelect(a: ContextAction) {
|
|
|
315
383
|
</button>
|
|
316
384
|
|
|
317
385
|
<button
|
|
318
|
-
v-for="a in
|
|
386
|
+
v-for="a in visibleToolbarItems"
|
|
319
387
|
:key="a.key"
|
|
320
388
|
type="button"
|
|
321
|
-
class="fe-btn"
|
|
389
|
+
class="fe-btn fe-btn--fold"
|
|
322
390
|
:class="{ 'fe-btn--danger': a.danger, 'is-disabled': a.disabled }"
|
|
323
391
|
:disabled="a.disabled"
|
|
324
392
|
:title="a.label"
|
|
@@ -328,6 +396,36 @@ function onMoreSelect(a: ContextAction) {
|
|
|
328
396
|
<span class="fe-btn__label">{{ a.label }}</span>
|
|
329
397
|
</button>
|
|
330
398
|
|
|
399
|
+
<!-- ui-fix — sığmayan aksiyonlar tek satırı korumak için ⋯ menüsüne
|
|
400
|
+
katlanır (arama/görünüm kontrolleri artık alt satıra düşmez). -->
|
|
401
|
+
<button
|
|
402
|
+
v-if="overflowToolbarItems.length > 0"
|
|
403
|
+
ref="wideMoreBtnEl"
|
|
404
|
+
type="button"
|
|
405
|
+
class="fe-btn fe-btn--icon-only"
|
|
406
|
+
:title="t('toolbar.more')"
|
|
407
|
+
:aria-label="t('toolbar.more')"
|
|
408
|
+
aria-haspopup="menu"
|
|
409
|
+
@click="openWideMore"
|
|
410
|
+
>
|
|
411
|
+
<span class="fe-icon">⋯</span>
|
|
412
|
+
</button>
|
|
413
|
+
|
|
414
|
+
<!-- Görünmez ölçüm şeridi: TÜM aksiyonlar her zaman burada render
|
|
415
|
+
edilir; katlama hesabı gerçek genişliklerden yapılır. -->
|
|
416
|
+
<div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
|
|
417
|
+
<button
|
|
418
|
+
v-for="a in toolbarItems"
|
|
419
|
+
:key="'m-' + a.key"
|
|
420
|
+
type="button"
|
|
421
|
+
class="fe-btn"
|
|
422
|
+
tabindex="-1"
|
|
423
|
+
>
|
|
424
|
+
<span class="fe-icon">{{ a.icon }}</span>
|
|
425
|
+
<span class="fe-btn__label">{{ a.label }}</span>
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
331
429
|
<button
|
|
332
430
|
v-if="pasteEnabled && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
|
|
333
431
|
type="button"
|
|
@@ -589,5 +687,16 @@ function onMoreSelect(a: ContextAction) {
|
|
|
589
687
|
@select="onMoreSelect"
|
|
590
688
|
/>
|
|
591
689
|
</template>
|
|
690
|
+
|
|
691
|
+
<!-- ui-fix — wide modda katlanan aksiyonların ⋯ menüsü -->
|
|
692
|
+
<ContextMenu
|
|
693
|
+
v-if="!narrow"
|
|
694
|
+
ref="wideMoreRef"
|
|
695
|
+
:locale="locale"
|
|
696
|
+
:theme="theme || 'auto'"
|
|
697
|
+
:sheet="false"
|
|
698
|
+
:actions="overflowToolbarItems"
|
|
699
|
+
@select="(a) => fire(a.key)"
|
|
700
|
+
/>
|
|
592
701
|
</div>
|
|
593
702
|
</template>
|
|
@@ -55,6 +55,13 @@ export interface ManagerResponse {
|
|
|
55
55
|
/** RBAC effective level for the current user on this directory ('' when ACL
|
|
56
56
|
* is not enforced on the storage). Gates the folder-level write actions. */
|
|
57
57
|
perm?: 'none' | 'viewer' | 'editor' | 'owner';
|
|
58
|
+
/* wiring:e2 — E2E-encrypted folder awareness: `e2e` is true when the listed
|
|
59
|
+
* dir IS an encrypted root; `e2e_root` is the adapter-qualified path of the
|
|
60
|
+
* nearest encrypted root covering this dir (set for the root itself AND for
|
|
61
|
+
* every subfolder inside the subtree). Absent on plain folders / old
|
|
62
|
+
* backends — consumers must stay undefined-safe. */
|
|
63
|
+
e2e?: boolean;
|
|
64
|
+
e2e_root?: string;
|
|
58
65
|
files: FileNode[];
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -341,6 +341,12 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
|
|
|
341
341
|
const root = rootEl.value;
|
|
342
342
|
if (!root) return;
|
|
343
343
|
|
|
344
|
+
/* ui-fix — while a context menu is open, global shortcuts stay quiet
|
|
345
|
+
* (except Esc, which the menu scopes itself). Otherwise e.g. Delete
|
|
346
|
+
* opens a modal UNDER the menu backdrop and the UI wedges: the
|
|
347
|
+
* backdrop intercepts every click on the new dialog. */
|
|
348
|
+
if (e.key !== 'Escape' && document.querySelector('.fe-ctx-backdrop')) return;
|
|
349
|
+
|
|
344
350
|
// Skip when the event originates inside a form control — don't
|
|
345
351
|
// want `Delete` while editing a filename, `/` while typing in the
|
|
346
352
|
// search box, etc. Escape always goes through so modals can close.
|
|
@@ -30,6 +30,9 @@ import type { ViewMode } from '../types/FileNode';
|
|
|
30
30
|
/** Per-tab split state — the secondary pane's own location. */
|
|
31
31
|
export interface TabSplit {
|
|
32
32
|
path: string;
|
|
33
|
+
/** ui-fix — the pane's OWN view mode (list/grid/gallery); undefined
|
|
34
|
+
* inherits the main panel's mode at split time. */
|
|
35
|
+
viewMode?: ViewMode;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface TabState {
|
|
@@ -82,10 +85,16 @@ export function useTabs(opts: UseTabsOptions) {
|
|
|
82
85
|
// gallery wave) must survive a round-trip through an older schema.
|
|
83
86
|
const vm =
|
|
84
87
|
typeof t.viewMode === 'string' && t.viewMode ? (t.viewMode as ViewMode) : 'list';
|
|
85
|
-
const rawSplit = t.split as { path?: unknown } | null | undefined;
|
|
88
|
+
const rawSplit = t.split as { path?: unknown; viewMode?: unknown } | null | undefined;
|
|
86
89
|
const split =
|
|
87
90
|
rawSplit && typeof rawSplit === 'object' && typeof rawSplit.path === 'string'
|
|
88
|
-
? {
|
|
91
|
+
? {
|
|
92
|
+
path: rawSplit.path,
|
|
93
|
+
viewMode:
|
|
94
|
+
typeof rawSplit.viewMode === 'string' && rawSplit.viewMode
|
|
95
|
+
? (rawSplit.viewMode as ViewMode)
|
|
96
|
+
: undefined,
|
|
97
|
+
}
|
|
89
98
|
: null;
|
|
90
99
|
clean.push({
|
|
91
100
|
id: typeof t.id === 'string' && t.id ? t.id : makeId(),
|
package/src/index.ts
CHANGED
|
@@ -133,3 +133,24 @@ export type { TabState, TabSplit, TabsApi } from './composables/useTabs';
|
|
|
133
133
|
export { default as TabBar } from './components/TabBar.vue';
|
|
134
134
|
export { default as SecondaryPane } from './components/SecondaryPane.vue';
|
|
135
135
|
/* /wiring:d1 */
|
|
136
|
+
/* wiring:e2 — uçtan uca şifreli klasörler (WebCrypto; docs/E2E-ENCRYPTION.md) */
|
|
137
|
+
export {
|
|
138
|
+
E2E_MARKER_NAME,
|
|
139
|
+
E2E_MAGIC,
|
|
140
|
+
E2E_VERSION,
|
|
141
|
+
E2E_DEFAULT_ITERATIONS,
|
|
142
|
+
E2E_MAX_FILE_BYTES,
|
|
143
|
+
E2E_MIN_PASSWORD_LEN,
|
|
144
|
+
E2eDecryptError,
|
|
145
|
+
deriveKek,
|
|
146
|
+
createMarker,
|
|
147
|
+
parseMarker,
|
|
148
|
+
verifyPassword,
|
|
149
|
+
hasMagic,
|
|
150
|
+
encryptFile,
|
|
151
|
+
decryptFile,
|
|
152
|
+
createKeyRing,
|
|
153
|
+
} from './lib/e2ecrypto';
|
|
154
|
+
export type { E2eMarker, E2eKeyRing } from './lib/e2ecrypto';
|
|
155
|
+
export { default as EncryptedFolderModal } from './components/EncryptedFolderModal.vue';
|
|
156
|
+
/* /wiring:e2 */
|