@brftech/filex-core 0.1.83 → 0.1.84
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-BRASXNIS.js → ArchiveViewer-wg4uE-3C.js} +2 -2
- package/dist/{ArchiveViewer-BRASXNIS.js.map → ArchiveViewer-wg4uE-3C.js.map} +1 -1
- package/dist/{CsvViewer-CgiLiHWw.js → CsvViewer-57uz4UKk.js} +2 -2
- package/dist/{CsvViewer-CgiLiHWw.js.map → CsvViewer-57uz4UKk.js.map} +1 -1
- package/dist/{DrawioViewer-D2I_uEng.js → DrawioViewer-BfL9Hbyp.js} +2 -2
- package/dist/{DrawioViewer-D2I_uEng.js.map → DrawioViewer-BfL9Hbyp.js.map} +1 -1
- package/dist/{EpubViewer-6nVPRR6R.js → EpubViewer-CYG2CV_3.js} +2 -2
- package/dist/{EpubViewer-6nVPRR6R.js.map → EpubViewer-CYG2CV_3.js.map} +1 -1
- package/dist/{IpynbViewer-B7p9hxFx.js → IpynbViewer-DJw34-wV.js} +2 -2
- package/dist/{IpynbViewer-B7p9hxFx.js.map → IpynbViewer-DJw34-wV.js.map} +1 -1
- package/dist/{MermaidViewer-O9vGvp4y.js → MermaidViewer-C86jjm27.js} +2 -2
- package/dist/{MermaidViewer-O9vGvp4y.js.map → MermaidViewer-C86jjm27.js.map} +1 -1
- package/dist/{PsdViewer-C6EaEAxF.js → PsdViewer-DGHIqv-x.js} +2 -2
- package/dist/{PsdViewer-C6EaEAxF.js.map → PsdViewer-DGHIqv-x.js.map} +1 -1
- package/dist/{TiffViewer-Dod4uIXI.js → TiffViewer-gct9Ub3o.js} +2 -2
- package/dist/{TiffViewer-Dod4uIXI.js.map → TiffViewer-gct9Ub3o.js.map} +1 -1
- package/dist/{Viewer3D-B3kLZrwO.js → Viewer3D-BVt4l2a9.js} +2 -2
- package/dist/{Viewer3D-B3kLZrwO.js.map → Viewer3D-BVt4l2a9.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +38 -38
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BeUoODQq.js +6871 -0
- package/dist/index-BeUoODQq.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +393 -31
- package/src/components/Breadcrumb.vue +100 -5
- package/src/components/CommandPalette.vue +299 -0
- package/src/components/GridView.vue +8 -13
- package/src/components/ListView.vue +173 -20
- package/src/components/ShortcutsHelp.vue +59 -0
- package/src/components/Toolbar.vue +65 -1
- package/src/composables/useKeyboardShortcuts.ts +38 -0
- package/src/composables/useRealtime.ts +9 -1
- package/src/lib/fileIcons.ts +127 -0
- package/src/locales/en.ts +54 -0
- package/src/locales/tr.ts +54 -0
- package/src/styles/base.css +549 -0
- package/src/styles/variables.css +70 -0
- package/dist/index-DL6_eaM3.js +0 -5888
- package/dist/index-DL6_eaM3.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -45,6 +45,10 @@ import GridView from './components/GridView.vue';
|
|
|
45
45
|
import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
|
|
46
46
|
import UploadProgress from './components/UploadProgress.vue';
|
|
47
47
|
import PendingOpsTray from './components/PendingOpsTray.vue';
|
|
48
|
+
/* cila:c wiring */
|
|
49
|
+
import CommandPalette from './components/CommandPalette.vue';
|
|
50
|
+
import ShortcutsHelp from './components/ShortcutsHelp.vue';
|
|
51
|
+
/* /cila:c wiring */
|
|
48
52
|
|
|
49
53
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
50
54
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -79,6 +83,12 @@ const emit = defineEmits<{
|
|
|
79
83
|
|
|
80
84
|
const api = useFileApi(props.config);
|
|
81
85
|
|
|
86
|
+
// Locale up-front: the pendingOps onSettled callback below (and the undo-toast
|
|
87
|
+
// helpers) need `t()` at runtime, so the catalogue must be constructed before
|
|
88
|
+
// they are wired. Depends only on props — safe this early.
|
|
89
|
+
const locale = computed(() => props.config.locale || 'tr');
|
|
90
|
+
const { t } = useLocale(locale);
|
|
91
|
+
|
|
82
92
|
// Live collaboration (WebSocket file-change events + presence), bundled into the
|
|
83
93
|
// core so every consumer — the native panel AND the embedded webcomponent —
|
|
84
94
|
// gets it. Auth is a short-lived ticket fetched through the same API (works
|
|
@@ -86,6 +96,10 @@ const api = useFileApi(props.config);
|
|
|
86
96
|
// socket is available.
|
|
87
97
|
const realtime = useRealtime(api, { reload: () => load() });
|
|
88
98
|
const presenceUsers = realtime.presenceUsers;
|
|
99
|
+
// True while the live socket is unavailable and the explorer runs on the
|
|
100
|
+
// polling fallback — drives the small "no live connection" badge. Healthy
|
|
101
|
+
// connections show nothing.
|
|
102
|
+
const realtimeDegraded = realtime.degraded;
|
|
89
103
|
function realtimeRoom(vp: string): string | null {
|
|
90
104
|
const p = (vp || '').replace(/^\/+|\/+$/g, '');
|
|
91
105
|
if (p === '.trash' || p.startsWith('.trash/')) return null;
|
|
@@ -112,10 +126,21 @@ onBeforeUnmount(() => realtime.stop());
|
|
|
112
126
|
const thumbs = useThumbs(props.config.apiBase, api);
|
|
113
127
|
|
|
114
128
|
const chunked = useUploadChunked(props.config, api);
|
|
129
|
+
|
|
130
|
+
// Undo registry for async pending ops: when a cleanly-invertible operation
|
|
131
|
+
// (move → reverse move, trash-delete → restore) is queued, its inverse is
|
|
132
|
+
// registered under the op id; once the op settles OK the toast grows a
|
|
133
|
+
// "Geri Al" action. Ops without an entry keep the plain settled toast.
|
|
134
|
+
const opUndo = new Map<number, { message: string; fn: () => Promise<void> }>();
|
|
135
|
+
|
|
115
136
|
const pendingOps = usePendingOps(props.config, api, {
|
|
116
137
|
onSettled: (op: PendingOp) => {
|
|
138
|
+
const undo = opUndo.get(op.id);
|
|
139
|
+
opUndo.delete(op.id);
|
|
117
140
|
if (op.status === 'error') {
|
|
118
141
|
flashToast(op.error_message || 'İşlem başarısız');
|
|
142
|
+
} else if (undo) {
|
|
143
|
+
undoToast(`${undo.message} (${op.progress_total})`, undo.fn);
|
|
119
144
|
} else {
|
|
120
145
|
const verb =
|
|
121
146
|
op.op_type === 'copy'
|
|
@@ -149,6 +174,16 @@ const dirPerm = ref<string>('');
|
|
|
149
174
|
// back 404 (folder doesn't exist) or 403 (RBAC-hidden — rendered identically
|
|
150
175
|
// on purpose so a denied folder doesn't reveal that it exists). '' = none.
|
|
151
176
|
const notFoundPath = ref<string>('');
|
|
177
|
+
// Listing failure that is NOT a dead link (network error, 5xx): remembered so
|
|
178
|
+
// the body can render a retryable error state instead of a misleading "this
|
|
179
|
+
// folder is empty". Only shown when no listing is visible — a failed
|
|
180
|
+
// navigation away from a healthy listing keeps the current list + toast,
|
|
181
|
+
// exactly as before.
|
|
182
|
+
const loadError = ref<string>('');
|
|
183
|
+
let loadErrorPath: string | undefined;
|
|
184
|
+
function retryLoad() {
|
|
185
|
+
void load(loadErrorPath);
|
|
186
|
+
}
|
|
152
187
|
|
|
153
188
|
const VIEW_MODE_KEY = 'brf-file-explorer:view-mode';
|
|
154
189
|
const viewMode = customRef<ViewMode>((track, trigger) => {
|
|
@@ -178,6 +213,9 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
|
|
|
178
213
|
},
|
|
179
214
|
};
|
|
180
215
|
});
|
|
216
|
+
/* cila:a density — Toolbar owns the persisted preference (filex.density);
|
|
217
|
+
mirrored here only so the root `.fe` can carry fe--density-compact. */
|
|
218
|
+
const density = ref<'comfortable' | 'compact'>('comfortable');
|
|
181
219
|
const searchQuery = ref('');
|
|
182
220
|
// trashMode — true while viewing the filex trash (soft-deleted nodes from the
|
|
183
221
|
// backend trash endpoint), entered by opening the virtual `.trash` row and
|
|
@@ -189,7 +227,6 @@ const trashMode = ref(false);
|
|
|
189
227
|
// global root). Set in loadTrash().
|
|
190
228
|
const trashOrigin = ref<string>('');
|
|
191
229
|
const trashActive = computed(() => trashMode.value);
|
|
192
|
-
const locale = computed(() => props.config.locale || 'tr');
|
|
193
230
|
|
|
194
231
|
// canGoUp/goUp — toolbar's "↑ Up one level" button. In single-storage
|
|
195
232
|
// mode "" means the storage root; in multi-storage mode "" means
|
|
@@ -224,8 +261,6 @@ function goUp() {
|
|
|
224
261
|
void load(parent);
|
|
225
262
|
}
|
|
226
263
|
|
|
227
|
-
const { t } = useLocale(locale);
|
|
228
|
-
|
|
229
264
|
const selection = useSelection(() => files.value);
|
|
230
265
|
watch(
|
|
231
266
|
() => [...selection.selected.value],
|
|
@@ -396,19 +431,57 @@ function selPerm(sel: FileNode[]): string {
|
|
|
396
431
|
// Can the current user write into the directory being viewed? Gates the
|
|
397
432
|
// toolbar New Folder / Upload / Paste + drag-drop upload.
|
|
398
433
|
const canWriteHere = computed(() => permCanEdit(dirPerm.value));
|
|
434
|
+
// Empty-state affordances: the "drop files here" hint + upload button only
|
|
435
|
+
// make sense in a real writable folder (not the virtual drives root, not the
|
|
436
|
+
// trash view).
|
|
437
|
+
const emptyCanUpload = computed(
|
|
438
|
+
() => canWriteHere.value && !atVirtualRoot.value && !trashMode.value,
|
|
439
|
+
);
|
|
399
440
|
|
|
400
441
|
// Context menu
|
|
401
442
|
const ctxRef = ref<InstanceType<typeof ContextMenu> | null>(null);
|
|
402
443
|
const rootEl = ref<HTMLElement | null>(null);
|
|
403
444
|
const toolbarRef = ref<InstanceType<typeof Toolbar> | null>(null);
|
|
404
445
|
|
|
405
|
-
// Toast (tiny, no lib)
|
|
406
|
-
|
|
446
|
+
// Toast (tiny, no lib). Evolved into a snackbar: plain messages keep the old
|
|
447
|
+
// 2.5s auto-hide; messages carrying an action ("Geri Al") stay 8s and can be
|
|
448
|
+
// dismissed by click or Esc.
|
|
449
|
+
interface ToastState {
|
|
450
|
+
message: string;
|
|
451
|
+
actionLabel?: string;
|
|
452
|
+
action?: () => void | Promise<void>;
|
|
453
|
+
}
|
|
454
|
+
const toast = ref<ToastState | null>(null);
|
|
407
455
|
let toastTimer: ReturnType<typeof setTimeout> | undefined;
|
|
408
|
-
function
|
|
409
|
-
toast.value =
|
|
456
|
+
function showToast(state: ToastState, ms: number) {
|
|
457
|
+
toast.value = state;
|
|
410
458
|
if (toastTimer) clearTimeout(toastTimer);
|
|
411
|
-
toastTimer = setTimeout(() => (toast.value = null),
|
|
459
|
+
toastTimer = setTimeout(() => (toast.value = null), ms);
|
|
460
|
+
}
|
|
461
|
+
function flashToast(msg: string) {
|
|
462
|
+
showToast({ message: msg }, 2500);
|
|
463
|
+
}
|
|
464
|
+
function undoToast(message: string, undo: () => Promise<void>) {
|
|
465
|
+
showToast({ message, actionLabel: t('toast.undo'), action: undo }, 8000);
|
|
466
|
+
}
|
|
467
|
+
function dismissToast() {
|
|
468
|
+
if (toastTimer) {
|
|
469
|
+
clearTimeout(toastTimer);
|
|
470
|
+
toastTimer = undefined;
|
|
471
|
+
}
|
|
472
|
+
toast.value = null;
|
|
473
|
+
}
|
|
474
|
+
async function runToastAction() {
|
|
475
|
+
const act = toast.value?.action;
|
|
476
|
+
dismissToast();
|
|
477
|
+
if (!act) return;
|
|
478
|
+
try {
|
|
479
|
+
await act();
|
|
480
|
+
flashToast(t('toast.undone'));
|
|
481
|
+
await load();
|
|
482
|
+
} catch {
|
|
483
|
+
flashToast(t('toast.undo_failed'));
|
|
484
|
+
}
|
|
412
485
|
}
|
|
413
486
|
|
|
414
487
|
// --------------------------------------------------------------------
|
|
@@ -477,6 +550,7 @@ async function load(path?: string) {
|
|
|
477
550
|
let requested = path ?? currentPath.value ?? '';
|
|
478
551
|
try {
|
|
479
552
|
notFoundPath.value = '';
|
|
553
|
+
loadError.value = '';
|
|
480
554
|
// Clamp to the confined floor: an empty/above-floor request (incl. a stale
|
|
481
555
|
// persisted path or the drives root) snaps back to rootPath. This both
|
|
482
556
|
// suppresses the multi-storage drives list and blocks up-navigation.
|
|
@@ -549,8 +623,13 @@ async function load(path?: string) {
|
|
|
549
623
|
emit('error', { message: e, context: { path } });
|
|
550
624
|
return;
|
|
551
625
|
}
|
|
626
|
+
// Real failure (network, 5xx). Never swallowed: the error still emits, and
|
|
627
|
+
// it surfaces either as the retryable error state (nothing else on screen)
|
|
628
|
+
// or as the classic toast over the still-visible previous listing.
|
|
629
|
+
loadError.value = e;
|
|
630
|
+
loadErrorPath = typeof requested === 'string' ? requested : undefined;
|
|
552
631
|
emit('error', { message: e, context: { path } });
|
|
553
|
-
flashToast(e);
|
|
632
|
+
if (files.value.length > 0) flashToast(e);
|
|
554
633
|
} finally {
|
|
555
634
|
loading.value = false;
|
|
556
635
|
}
|
|
@@ -656,6 +735,53 @@ function qualify(p: string): string {
|
|
|
656
735
|
return `${adapter.value}://${p.replace(/^\/+/, '')}`;
|
|
657
736
|
}
|
|
658
737
|
|
|
738
|
+
// ----------------------------------------------------------------
|
|
739
|
+
// Undo helpers — compute the inverse of cleanly-invertible operations
|
|
740
|
+
// (move → reverse move, rename → rename back, trash → restore). All
|
|
741
|
+
// paths here are wire form (`<adapter>://<rel>`).
|
|
742
|
+
// ----------------------------------------------------------------
|
|
743
|
+
|
|
744
|
+
function wireBasename(p: string): string {
|
|
745
|
+
const idx = p.indexOf('://');
|
|
746
|
+
const rel = (idx === -1 ? p : p.slice(idx + 3)).replace(/\/+$/, '');
|
|
747
|
+
const slash = rel.lastIndexOf('/');
|
|
748
|
+
return slash === -1 ? rel : rel.slice(slash + 1);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function wireParent(p: string): string {
|
|
752
|
+
const idx = p.indexOf('://');
|
|
753
|
+
const prefix = idx === -1 ? '' : p.slice(0, idx + 3);
|
|
754
|
+
const rel = (idx === -1 ? p : p.slice(idx + 3)).replace(/\/+$/, '');
|
|
755
|
+
const slash = rel.lastIndexOf('/');
|
|
756
|
+
return slash === -1 ? prefix : prefix + rel.slice(0, slash);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function wireJoin(dir: string, name: string): string {
|
|
760
|
+
if (!dir) return name;
|
|
761
|
+
return dir.endsWith('://') || dir.endsWith('/') ? dir + name : `${dir}/${name}`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Register the inverse of a queued async move under its op id: once the op
|
|
765
|
+
// settles OK, the toast offers "Geri Al" which queues the reverse move. The
|
|
766
|
+
// inverse op deliberately gets NO undo entry of its own (no redo ping-pong).
|
|
767
|
+
function registerMoveUndo(
|
|
768
|
+
opId: number,
|
|
769
|
+
sources: string[],
|
|
770
|
+
targetWire: string,
|
|
771
|
+
originWire: string | undefined,
|
|
772
|
+
) {
|
|
773
|
+
if (!originWire || !targetWire) return;
|
|
774
|
+
const movedPaths = sources.map((p) => wireJoin(targetWire, wireBasename(p)));
|
|
775
|
+
if (movedPaths.length === 0) return;
|
|
776
|
+
opUndo.set(opId, {
|
|
777
|
+
message: t('toast.moved'),
|
|
778
|
+
fn: async () => {
|
|
779
|
+
const { op } = await api.moveAsync(movedPaths, originWire, targetWire);
|
|
780
|
+
pendingOps.register(op);
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
659
785
|
watch(
|
|
660
786
|
() => searchQuery.value,
|
|
661
787
|
() => void load(),
|
|
@@ -793,6 +919,11 @@ onMounted(async () => {
|
|
|
793
919
|
// Keyboard
|
|
794
920
|
// --------------------------------------------------------------------
|
|
795
921
|
|
|
922
|
+
/* cila:c wiring — command palette (Ctrl/Cmd+K) + shortcuts help (?) state */
|
|
923
|
+
const showPalette = ref(false);
|
|
924
|
+
const showShortcutsHelp = ref(false);
|
|
925
|
+
/* /cila:c wiring */
|
|
926
|
+
|
|
796
927
|
useKeyboardShortcuts(rootEl, {
|
|
797
928
|
onDelete: () => {
|
|
798
929
|
if (!selection.isEmpty.value) showDelete.value = true;
|
|
@@ -815,12 +946,21 @@ useKeyboardShortcuts(rootEl, {
|
|
|
815
946
|
showShare.value = false;
|
|
816
947
|
showPreview.value = false;
|
|
817
948
|
ctxRef.value?.hide();
|
|
949
|
+
dismissToast();
|
|
818
950
|
},
|
|
819
951
|
onFocusSearch: () => toolbarRef.value?.focusSearch(),
|
|
820
952
|
onCut: () => cut(),
|
|
821
953
|
onCopy: () => copyToClipboard(),
|
|
822
954
|
onPaste: () => paste(),
|
|
823
955
|
onGoUp: () => goUp(),
|
|
956
|
+
/* cila:c wiring */
|
|
957
|
+
onPathJump: () => {
|
|
958
|
+
showPalette.value = true;
|
|
959
|
+
},
|
|
960
|
+
onShowHelp: () => {
|
|
961
|
+
showShortcutsHelp.value = true;
|
|
962
|
+
},
|
|
963
|
+
/* /cila:c wiring */
|
|
824
964
|
hasSelection: () => !selection.isEmpty.value,
|
|
825
965
|
});
|
|
826
966
|
|
|
@@ -1264,7 +1404,10 @@ async function paste() {
|
|
|
1264
1404
|
}
|
|
1265
1405
|
|
|
1266
1406
|
if (cb.mode === 'cut') {
|
|
1267
|
-
const
|
|
1407
|
+
const targetWire = qualify(currentPath.value);
|
|
1408
|
+
const originWire = qualify(sourceDir) || undefined;
|
|
1409
|
+
const { op } = await api.moveAsync(items, targetWire, originWire);
|
|
1410
|
+
registerMoveUndo(op.id, items, targetWire, originWire);
|
|
1268
1411
|
pendingOps.register(op);
|
|
1269
1412
|
flashToast('Taşıma kuyruğa alındı');
|
|
1270
1413
|
} else {
|
|
@@ -1311,10 +1454,20 @@ async function submitRename(name: string) {
|
|
|
1311
1454
|
const target = renameTarget.value;
|
|
1312
1455
|
if (!target) return;
|
|
1313
1456
|
try {
|
|
1314
|
-
|
|
1457
|
+
const dirWire = qualify(currentPath.value);
|
|
1458
|
+
const oldPath = target.path; // qualified
|
|
1459
|
+
const oldName = target.basename;
|
|
1460
|
+
await api.rename(dirWire, oldPath, name);
|
|
1315
1461
|
showRename.value = false;
|
|
1316
1462
|
renameTarget.value = null;
|
|
1317
1463
|
await load();
|
|
1464
|
+
// Clean inverse: rename the new path back to the old basename.
|
|
1465
|
+
if (name && name !== oldName) {
|
|
1466
|
+
const newPath = wireJoin(wireParent(oldPath), name);
|
|
1467
|
+
undoToast(t('toast.renamed'), async () => {
|
|
1468
|
+
await api.rename(dirWire, newPath, oldName);
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1318
1471
|
} catch (err) {
|
|
1319
1472
|
emit('error', { message: (err as Error).message, context: { op: 'rename' } });
|
|
1320
1473
|
}
|
|
@@ -1329,19 +1482,37 @@ async function confirmDelete() {
|
|
|
1329
1482
|
flashToast('Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.');
|
|
1330
1483
|
return;
|
|
1331
1484
|
}
|
|
1332
|
-
const
|
|
1485
|
+
const targets = selection.nodes.value;
|
|
1486
|
+
const items = targets.map((n) => n.path);
|
|
1333
1487
|
if (items.length === 0) {
|
|
1334
1488
|
showDelete.value = false;
|
|
1335
1489
|
return;
|
|
1336
1490
|
}
|
|
1491
|
+
// Trash-delete is invertible via node-id restore — but only when EVERY
|
|
1492
|
+
// selected node carries a backend id and the restore endpoint exists.
|
|
1493
|
+
// A partial-undo offer would be a lie, so all-or-nothing.
|
|
1494
|
+
const nodeIds = targets
|
|
1495
|
+
.map((n) => (n as { id?: number }).id)
|
|
1496
|
+
.filter((x): x is number => typeof x === 'number');
|
|
1497
|
+
const restoreUndo =
|
|
1498
|
+
api.endpoints.trashRestore && nodeIds.length === targets.length
|
|
1499
|
+
? async () => {
|
|
1500
|
+
const { restored } = await api.restoreIds(nodeIds);
|
|
1501
|
+
if (restored === 0) throw new Error('restore failed');
|
|
1502
|
+
}
|
|
1503
|
+
: null;
|
|
1337
1504
|
try {
|
|
1338
1505
|
if (api.endpoints.deleteAsync) {
|
|
1339
1506
|
const { op } = await api.deleteAsync(items, qualify(currentPath.value));
|
|
1507
|
+
if (restoreUndo) {
|
|
1508
|
+
opUndo.set(op.id, { message: t('toast.trashed'), fn: restoreUndo });
|
|
1509
|
+
}
|
|
1340
1510
|
pendingOps.register(op);
|
|
1341
1511
|
flashToast('Silme kuyruğa alındı');
|
|
1342
1512
|
} else {
|
|
1343
1513
|
await api.deleteItems(qualify(currentPath.value), items);
|
|
1344
1514
|
await load();
|
|
1515
|
+
if (restoreUndo) undoToast(t('toast.trashed'), restoreUndo);
|
|
1345
1516
|
}
|
|
1346
1517
|
showDelete.value = false;
|
|
1347
1518
|
selection.clear();
|
|
@@ -1635,13 +1806,20 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
|
|
|
1635
1806
|
|
|
1636
1807
|
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string): Promise<void> {
|
|
1637
1808
|
try {
|
|
1809
|
+
const originWire = qualify(currentPath.value);
|
|
1638
1810
|
if (api.endpoints.moveAsync) {
|
|
1639
|
-
const { op } = await api.moveAsync(sources, targetDir,
|
|
1811
|
+
const { op } = await api.moveAsync(sources, targetDir, originWire);
|
|
1812
|
+
registerMoveUndo(op.id, sources, targetDir, originWire);
|
|
1640
1813
|
pendingOps.register(op);
|
|
1641
1814
|
flashToast('Taşıma kuyruğa alındı');
|
|
1642
1815
|
} else {
|
|
1643
|
-
await api.move(
|
|
1816
|
+
await api.move(originWire, sources, targetDir);
|
|
1644
1817
|
await load();
|
|
1818
|
+
// Sync move (no async endpoint): offer the reverse move right away.
|
|
1819
|
+
const movedPaths = sources.map((p) => wireJoin(targetDir, wireBasename(p)));
|
|
1820
|
+
undoToast(t('toast.moved'), async () => {
|
|
1821
|
+
await api.move(targetDir, movedPaths, originWire);
|
|
1822
|
+
});
|
|
1645
1823
|
}
|
|
1646
1824
|
selection.clear();
|
|
1647
1825
|
} catch (err) {
|
|
@@ -1741,6 +1919,7 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
1741
1919
|
'fe--theme-light': config.theme === 'light',
|
|
1742
1920
|
'fe--theme-dark': config.theme === 'dark',
|
|
1743
1921
|
'fe--is-dragover': dragOver,
|
|
1922
|
+
'fe--density-compact': density === 'compact' /* cila:a density */,
|
|
1744
1923
|
}"
|
|
1745
1924
|
tabindex="-1"
|
|
1746
1925
|
@dragenter="onDragEnter"
|
|
@@ -1764,6 +1943,7 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
1764
1943
|
:locale="locale"
|
|
1765
1944
|
@update:view-mode="viewMode = $event"
|
|
1766
1945
|
@update:search-query="searchQuery = $event"
|
|
1946
|
+
@update:density="density = $event"
|
|
1767
1947
|
@new-folder="showNewFolder = true"
|
|
1768
1948
|
@upload="triggerUpload"
|
|
1769
1949
|
@refresh="() => load()"
|
|
@@ -1785,28 +1965,171 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
1785
1965
|
@crumb-drop="onCrumbDropInto"
|
|
1786
1966
|
/>
|
|
1787
1967
|
|
|
1788
|
-
<!-- Live presence: who else is viewing this folder (empty → nothing shown).
|
|
1789
|
-
|
|
1790
|
-
|
|
1968
|
+
<!-- Live presence: who else is viewing this folder (empty → nothing shown).
|
|
1969
|
+
When the live socket is unavailable the same strip carries a small
|
|
1970
|
+
degraded-connection badge instead (presence is empty in fallback);
|
|
1971
|
+
a healthy connection shows nothing extra. -->
|
|
1972
|
+
<div v-if="presenceUsers.length || realtimeDegraded" class="fe__presence">
|
|
1973
|
+
<PresenceBar v-if="presenceUsers.length" :users="presenceUsers" :locale="locale" />
|
|
1974
|
+
<span
|
|
1975
|
+
v-if="realtimeDegraded"
|
|
1976
|
+
class="fe-connbadge"
|
|
1977
|
+
role="status"
|
|
1978
|
+
:title="t('conn.tooltip')"
|
|
1979
|
+
>
|
|
1980
|
+
<span class="fe-connbadge__dot" aria-hidden="true"></span>
|
|
1981
|
+
{{ t('conn.offline') }}
|
|
1982
|
+
</span>
|
|
1791
1983
|
</div>
|
|
1792
1984
|
|
|
1793
1985
|
<div class="fe__body" @click.self="selection.clear()">
|
|
1794
|
-
<!-- Initial load:
|
|
1795
|
-
Only when there's nothing yet — navigation
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
<
|
|
1986
|
+
<!-- Initial load: skeleton ghosts (view-mode aware) instead of an
|
|
1987
|
+
empty/"no files" flash. Only when there's nothing yet — navigation
|
|
1988
|
+
keeps the current list, exactly as before. -->
|
|
1989
|
+
<div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
|
|
1990
|
+
<span class="fe-sr-only">{{ t('loading') }}</span>
|
|
1991
|
+
<div v-if="viewMode === 'grid'" class="fe-skel-grid" aria-hidden="true">
|
|
1992
|
+
<div v-for="i in 8" :key="i" class="fe-skel-card">
|
|
1993
|
+
<div class="fe-skel fe-skel--thumb"></div>
|
|
1994
|
+
<div class="fe-skel fe-skel--label"></div>
|
|
1995
|
+
</div>
|
|
1996
|
+
</div>
|
|
1997
|
+
<div v-else class="fe-skel-list" aria-hidden="true">
|
|
1998
|
+
<div v-for="i in 8" :key="i" class="fe-skel-row">
|
|
1999
|
+
<div class="fe-skel fe-skel--icon"></div>
|
|
2000
|
+
<div class="fe-skel fe-skel--name"></div>
|
|
2001
|
+
<div class="fe-skel fe-skel--size"></div>
|
|
2002
|
+
<div class="fe-skel fe-skel--date"></div>
|
|
2003
|
+
</div>
|
|
2004
|
+
</div>
|
|
1799
2005
|
</div>
|
|
1800
2006
|
<!-- Dead deep link (404) or RBAC-hidden dir (403, shown identically):
|
|
1801
2007
|
a dedicated state instead of a misleading "this folder is empty". -->
|
|
1802
|
-
<div v-else-if="notFoundPath" class="
|
|
1803
|
-
<
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2008
|
+
<div v-else-if="notFoundPath" class="fe-state">
|
|
2009
|
+
<svg
|
|
2010
|
+
class="fe-state__art"
|
|
2011
|
+
viewBox="0 0 120 100"
|
|
2012
|
+
width="110"
|
|
2013
|
+
height="92"
|
|
2014
|
+
fill="none"
|
|
2015
|
+
stroke="currentColor"
|
|
2016
|
+
stroke-width="2"
|
|
2017
|
+
stroke-linecap="round"
|
|
2018
|
+
stroke-linejoin="round"
|
|
2019
|
+
aria-hidden="true"
|
|
2020
|
+
>
|
|
2021
|
+
<path d="M18 36v42a6 6 0 0 0 6 6h72a6 6 0 0 0 6-6V44a6 6 0 0 0-6-6H62l-9-10H24a6 6 0 0 0-6 6z" />
|
|
2022
|
+
<path d="M52 55c0-4.6 3.6-8 8-8s8 3.4 8 8c0 5.5-8 4.8-8 11" />
|
|
2023
|
+
<circle cx="60" cy="73" r="1.6" fill="currentColor" stroke="none" />
|
|
2024
|
+
</svg>
|
|
2025
|
+
<p class="fe-state__title">{{ t('notFound.title') }}</p>
|
|
2026
|
+
<p class="fe-state__path">{{ notFoundPath }}</p>
|
|
2027
|
+
<p class="fe-state__hint">{{ t('notFound.desc') }}</p>
|
|
2028
|
+
<div class="fe-state__actions">
|
|
2029
|
+
<button type="button" class="fe-btn" @click="leaveNotFound">
|
|
2030
|
+
{{ t('notFound.goRoot') }}
|
|
2031
|
+
</button>
|
|
2032
|
+
</div>
|
|
2033
|
+
</div>
|
|
2034
|
+
<!-- Listing failed (network / 5xx) with nothing else to show: retryable
|
|
2035
|
+
error state in the same visual language. -->
|
|
2036
|
+
<div v-else-if="loadError && files.length === 0" class="fe-state">
|
|
2037
|
+
<svg
|
|
2038
|
+
class="fe-state__art"
|
|
2039
|
+
viewBox="0 0 120 100"
|
|
2040
|
+
width="110"
|
|
2041
|
+
height="92"
|
|
2042
|
+
fill="none"
|
|
2043
|
+
stroke="currentColor"
|
|
2044
|
+
stroke-width="2"
|
|
2045
|
+
stroke-linecap="round"
|
|
2046
|
+
stroke-linejoin="round"
|
|
2047
|
+
aria-hidden="true"
|
|
2048
|
+
>
|
|
2049
|
+
<circle cx="60" cy="50" r="28" />
|
|
2050
|
+
<path d="M60 36v18" />
|
|
2051
|
+
<circle cx="60" cy="63" r="1.8" fill="currentColor" stroke="none" />
|
|
2052
|
+
<path d="M24 88h72" stroke-dasharray="3 5" />
|
|
2053
|
+
</svg>
|
|
2054
|
+
<p class="fe-state__title">{{ t('error.title') }}</p>
|
|
2055
|
+
<p class="fe-state__hint">{{ loadError }}</p>
|
|
2056
|
+
<div class="fe-state__actions">
|
|
2057
|
+
<button type="button" class="fe-btn fe-btn--primary" @click="retryLoad">
|
|
2058
|
+
{{ t('error.retry') }}
|
|
2059
|
+
</button>
|
|
2060
|
+
</div>
|
|
2061
|
+
</div>
|
|
2062
|
+
<!-- Search with zero hits — its own message, not "folder is empty". -->
|
|
2063
|
+
<div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
|
|
2064
|
+
<svg
|
|
2065
|
+
class="fe-state__art"
|
|
2066
|
+
viewBox="0 0 120 100"
|
|
2067
|
+
width="110"
|
|
2068
|
+
height="92"
|
|
2069
|
+
fill="none"
|
|
2070
|
+
stroke="currentColor"
|
|
2071
|
+
stroke-width="2"
|
|
2072
|
+
stroke-linecap="round"
|
|
2073
|
+
stroke-linejoin="round"
|
|
2074
|
+
aria-hidden="true"
|
|
2075
|
+
>
|
|
2076
|
+
<circle cx="52" cy="44" r="22" />
|
|
2077
|
+
<path d="M68 61l20 20" />
|
|
2078
|
+
<path d="M46 38l12 12M58 38l-12 12" />
|
|
2079
|
+
</svg>
|
|
2080
|
+
<p class="fe-state__title">{{ t('empty.search.title') }}</p>
|
|
2081
|
+
<p class="fe-state__hint">{{ t('empty.search.hint') }}</p>
|
|
2082
|
+
</div>
|
|
2083
|
+
<!-- Empty trash view. -->
|
|
2084
|
+
<div v-else-if="!loading && files.length === 0 && trashMode" class="fe-state">
|
|
2085
|
+
<svg
|
|
2086
|
+
class="fe-state__art"
|
|
2087
|
+
viewBox="0 0 120 100"
|
|
2088
|
+
width="110"
|
|
2089
|
+
height="92"
|
|
2090
|
+
fill="none"
|
|
2091
|
+
stroke="currentColor"
|
|
2092
|
+
stroke-width="2"
|
|
2093
|
+
stroke-linecap="round"
|
|
2094
|
+
stroke-linejoin="round"
|
|
2095
|
+
aria-hidden="true"
|
|
2096
|
+
>
|
|
2097
|
+
<path d="M38 34l4 48a6 6 0 0 0 6 5.6h24a6 6 0 0 0 6-5.6l4-48" />
|
|
2098
|
+
<path d="M32 34h56" />
|
|
2099
|
+
<path d="M50 34v-6a6 6 0 0 1 6-6h8a6 6 0 0 1 6 6v6" />
|
|
2100
|
+
<path d="M52 44v32M60 44v32M68 44v32" opacity="0.5" />
|
|
2101
|
+
</svg>
|
|
2102
|
+
<p class="fe-state__title">{{ t('empty.trash.title') }}</p>
|
|
2103
|
+
</div>
|
|
2104
|
+
<!-- Loaded, zero files, no search: the real empty-folder state. The
|
|
2105
|
+
upload affordances follow write permission (RBAC viewers only get
|
|
2106
|
+
the title). -->
|
|
2107
|
+
<div v-else-if="!loading && files.length === 0" class="fe-state">
|
|
2108
|
+
<svg
|
|
2109
|
+
class="fe-state__art"
|
|
2110
|
+
viewBox="0 0 120 100"
|
|
2111
|
+
width="110"
|
|
2112
|
+
height="92"
|
|
2113
|
+
fill="none"
|
|
2114
|
+
stroke="currentColor"
|
|
2115
|
+
stroke-width="2"
|
|
2116
|
+
stroke-linecap="round"
|
|
2117
|
+
stroke-linejoin="round"
|
|
2118
|
+
aria-hidden="true"
|
|
2119
|
+
>
|
|
2120
|
+
<path d="M18 36v42a6 6 0 0 0 6 6h72a6 6 0 0 0 6-6V44a6 6 0 0 0-6-6H62l-9-10H24a6 6 0 0 0-6 6z" />
|
|
2121
|
+
<g v-if="emptyCanUpload">
|
|
2122
|
+
<path d="M60 50v14" stroke-dasharray="3 4" />
|
|
2123
|
+
<path d="M53 59l7 8 7-8" />
|
|
2124
|
+
</g>
|
|
2125
|
+
</svg>
|
|
2126
|
+
<p class="fe-state__title">{{ t('empty.folder') }}</p>
|
|
2127
|
+
<p v-if="emptyCanUpload" class="fe-state__hint">{{ t('empty.hint') }}</p>
|
|
2128
|
+
<div v-if="emptyCanUpload" class="fe-state__actions">
|
|
2129
|
+
<button type="button" class="fe-btn fe-btn--primary" @click="triggerUpload">
|
|
2130
|
+
{{ t('empty.upload') }}
|
|
2131
|
+
</button>
|
|
2132
|
+
</div>
|
|
1810
2133
|
</div>
|
|
1811
2134
|
<ListView
|
|
1812
2135
|
v-else-if="viewMode === 'list'"
|
|
@@ -1995,6 +2318,31 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
1995
2318
|
</div>
|
|
1996
2319
|
</transition>
|
|
1997
2320
|
|
|
2321
|
+
<!-- cila:c wiring — command palette (Ctrl/Cmd+K) + shortcuts help (?) -->
|
|
2322
|
+
<CommandPalette
|
|
2323
|
+
:open="showPalette"
|
|
2324
|
+
:locale="locale"
|
|
2325
|
+
:files="files"
|
|
2326
|
+
:view-mode="viewMode"
|
|
2327
|
+
:can-write="canWriteHere && !atVirtualRoot && !trashActive"
|
|
2328
|
+
:can-go-up="canGoUp"
|
|
2329
|
+
@close="showPalette = false"
|
|
2330
|
+
@open-node="openNode"
|
|
2331
|
+
@navigate="(p: string) => load(p)"
|
|
2332
|
+
@new-folder="showNewFolder = true"
|
|
2333
|
+
@upload="triggerUpload"
|
|
2334
|
+
@toggle-view="viewMode = viewMode === 'list' ? 'grid' : 'list'"
|
|
2335
|
+
@open-trash="loadTrash"
|
|
2336
|
+
@refresh="() => load()"
|
|
2337
|
+
@go-up="goUp"
|
|
2338
|
+
/>
|
|
2339
|
+
<ShortcutsHelp
|
|
2340
|
+
:open="showShortcutsHelp"
|
|
2341
|
+
:locale="locale"
|
|
2342
|
+
@close="showShortcutsHelp = false"
|
|
2343
|
+
/>
|
|
2344
|
+
<!-- /cila:c wiring -->
|
|
2345
|
+
|
|
1998
2346
|
<input
|
|
1999
2347
|
ref="fileInputEl"
|
|
2000
2348
|
type="file"
|
|
@@ -2004,7 +2352,21 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2004
2352
|
/>
|
|
2005
2353
|
|
|
2006
2354
|
<transition name="fe-toast">
|
|
2007
|
-
<div
|
|
2355
|
+
<div
|
|
2356
|
+
v-if="toast"
|
|
2357
|
+
class="fe-toast"
|
|
2358
|
+
:class="{ 'fe-toast--action': !!toast.actionLabel }"
|
|
2359
|
+
role="status"
|
|
2360
|
+
@click="dismissToast"
|
|
2361
|
+
>
|
|
2362
|
+
<span class="fe-toast__msg">{{ toast.message }}</span>
|
|
2363
|
+
<button
|
|
2364
|
+
v-if="toast.actionLabel && toast.action"
|
|
2365
|
+
type="button"
|
|
2366
|
+
class="fe-toast__action"
|
|
2367
|
+
@click.stop="runToastAction"
|
|
2368
|
+
>{{ toast.actionLabel }}</button>
|
|
2369
|
+
</div>
|
|
2008
2370
|
</transition>
|
|
2009
2371
|
</div>
|
|
2010
2372
|
</template>
|