@brftech/filex-core 0.7.2 → 0.7.4
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-DAb5peBU.js → ArchiveViewer-BRFvhWwL.js} +2 -2
- package/dist/{ArchiveViewer-DAb5peBU.js.map → ArchiveViewer-BRFvhWwL.js.map} +1 -1
- package/dist/{CsvViewer-_bnmnpjn.js → CsvViewer-1cGGKeGi.js} +2 -2
- package/dist/{CsvViewer-_bnmnpjn.js.map → CsvViewer-1cGGKeGi.js.map} +1 -1
- package/dist/{DrawioViewer-Bjns4ob8.js → DrawioViewer-C0oThT59.js} +2 -2
- package/dist/{DrawioViewer-Bjns4ob8.js.map → DrawioViewer-C0oThT59.js.map} +1 -1
- package/dist/{EpubViewer-DoFuZTtA.js → EpubViewer-DCPOD2S-.js} +2 -2
- package/dist/{EpubViewer-DoFuZTtA.js.map → EpubViewer-DCPOD2S-.js.map} +1 -1
- package/dist/{IpynbViewer-eg1ySMD4.js → IpynbViewer-mv4xiv3H.js} +2 -2
- package/dist/{IpynbViewer-eg1ySMD4.js.map → IpynbViewer-mv4xiv3H.js.map} +1 -1
- package/dist/{MermaidViewer-BZ13vA6S.js → MermaidViewer-BBNVghTz.js} +2 -2
- package/dist/{MermaidViewer-BZ13vA6S.js.map → MermaidViewer-BBNVghTz.js.map} +1 -1
- package/dist/{PsdViewer-qFkE4KCB.js → PsdViewer-B8ygWw-s.js} +2 -2
- package/dist/{PsdViewer-qFkE4KCB.js.map → PsdViewer-B8ygWw-s.js.map} +1 -1
- package/dist/{TiffViewer-eHJTCYsd.js → TiffViewer-BhHjf3s5.js} +2 -2
- package/dist/{TiffViewer-eHJTCYsd.js.map → TiffViewer-BhHjf3s5.js.map} +1 -1
- package/dist/{Viewer3D-C4--ljdO.js → Viewer3D-BhOJpzyG.js} +2 -2
- package/dist/{Viewer3D-C4--ljdO.js.map → Viewer3D-BhOJpzyG.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +34 -34
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-BSDT0PU8.js → index-BjiUhk9K.js} +4008 -3989
- package/dist/index-BjiUhk9K.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +123 -123
- package/src/components/SecondaryPane.vue +21 -9
- package/src/lib/listing.ts +105 -0
- package/src/styles/base.css +11 -5
- package/dist/index-BSDT0PU8.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -86,6 +86,15 @@ import {
|
|
|
86
86
|
} from './lib/e2ecrypto';
|
|
87
87
|
/* /wiring:e2 */
|
|
88
88
|
|
|
89
|
+
/* ui-fix — listing helpers shared with SecondaryPane (single source: the
|
|
90
|
+
internal-entry filter + virtual `.trash` row must be identical in both
|
|
91
|
+
panes or split view shows mismatched rows). */
|
|
92
|
+
import {
|
|
93
|
+
filterInternalEntries,
|
|
94
|
+
injectTrashRow,
|
|
95
|
+
hydrateTrashRow as hydrateTrashRowShared,
|
|
96
|
+
} from './lib/listing';
|
|
97
|
+
|
|
89
98
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
90
99
|
import RenameModal from './modals/RenameModal.vue';
|
|
91
100
|
import DeleteConfirmModal from './modals/DeleteConfirmModal.vue';
|
|
@@ -438,6 +447,9 @@ const showDelete = ref(false);
|
|
|
438
447
|
const showShare = ref(false);
|
|
439
448
|
const showPreview = ref(false);
|
|
440
449
|
const renameTarget = ref<FileNode | null>(null);
|
|
450
|
+
/* ui-fix — açık olan rename/delete/new-folder modal'ı yan panele mi ait?
|
|
451
|
+
* (menü ana panelle aynı; mutasyonlar doğru panele yönlensin diye.) */
|
|
452
|
+
const mutationInPane = ref(false);
|
|
441
453
|
const shareTarget = ref<FileNode | null>(null);
|
|
442
454
|
const activeShare = ref<(ShareInfo & { url: string; filename?: string }) | null>(null);
|
|
443
455
|
const previewTarget = ref<FileNode | null>(null);
|
|
@@ -704,33 +716,11 @@ async function load(path?: string) {
|
|
|
704
716
|
subtree; '' resets on every plain folder. Drives the lock screen. */
|
|
705
717
|
e2eRoot.value = typeof resp.e2e_root === 'string' ? resp.e2e_root : '';
|
|
706
718
|
/* /wiring:e2 */
|
|
707
|
-
files.value = (resp.files
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (f.basename === E2E_MARKER_NAME) return false; /* wiring:e2 — marker gizli */
|
|
713
|
-
return true;
|
|
714
|
-
});
|
|
715
|
-
// Inject virtual `.trash` entry at root only.
|
|
716
|
-
const dirRel = stripAdapter(resp.dirname);
|
|
717
|
-
const inRoot = dirRel === 'fileman' || dirRel === '';
|
|
718
|
-
const isTrashListing = dirRel.startsWith('fileman/.trash');
|
|
719
|
-
const trashEntryEnabled = props.config.trashVisible !== false;
|
|
720
|
-
if (!isTrashListing && inRoot && trashEntryEnabled) {
|
|
721
|
-
files.value.unshift({
|
|
722
|
-
type: 'dir',
|
|
723
|
-
path: `${resp.adapter}://fileman/.trash`,
|
|
724
|
-
basename: '.trash',
|
|
725
|
-
extension: '',
|
|
726
|
-
storage: resp.adapter,
|
|
727
|
-
visibility: 'private',
|
|
728
|
-
size: 0,
|
|
729
|
-
file_size: 0,
|
|
730
|
-
mime_type: 'inode/directory',
|
|
731
|
-
extra_metadata: {},
|
|
732
|
-
} as unknown as FileNode);
|
|
733
|
-
void hydrateTrashRow(resp.adapter);
|
|
719
|
+
files.value = filterInternalEntries(resp.files);
|
|
720
|
+
// Inject virtual `.trash` entry at root only — shared helper so the
|
|
721
|
+
// split-view secondary pane shows the exact same row (no row-offset).
|
|
722
|
+
if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.config.trashVisible !== false)) {
|
|
723
|
+
void hydrateTrashRowShared(files.value, resp.adapter, api);
|
|
734
724
|
}
|
|
735
725
|
// currentPath is the user-facing form: `s3-test/example` in
|
|
736
726
|
// multi-storage mode, the bare relative path otherwise.
|
|
@@ -781,28 +771,7 @@ function leaveNotFound() {
|
|
|
781
771
|
void load('');
|
|
782
772
|
}
|
|
783
773
|
|
|
784
|
-
// hydrateTrashRow
|
|
785
|
-
// fill both from the backend trash listing (total bytes + newest deletion) so
|
|
786
|
-
// the row reads like a real folder instead of "— / —". Best-effort and
|
|
787
|
-
// non-blocking: the row appears immediately and updates when the listing lands.
|
|
788
|
-
async function hydrateTrashRow(storage: string) {
|
|
789
|
-
try {
|
|
790
|
-
const { entries } = await api.listTrash(storage);
|
|
791
|
-
const row = files.value.find((f) => f.basename === '.trash');
|
|
792
|
-
if (!row) return;
|
|
793
|
-
let total = 0;
|
|
794
|
-
let newest = 0;
|
|
795
|
-
for (const e of entries) {
|
|
796
|
-
total += e.size || 0;
|
|
797
|
-
const ts = Date.parse(e.deleted_at);
|
|
798
|
-
if (!Number.isNaN(ts) && ts > newest) newest = ts;
|
|
799
|
-
}
|
|
800
|
-
row.size = total;
|
|
801
|
-
if (newest > 0) row.last_modified = newest;
|
|
802
|
-
} catch {
|
|
803
|
-
/* keep the bare row */
|
|
804
|
-
}
|
|
805
|
-
}
|
|
774
|
+
// (hydrateTrashRow moved to lib/listing.ts — shared with SecondaryPane.)
|
|
806
775
|
|
|
807
776
|
// loadTrash — show the backend trash (soft-deleted nodes) as a flat listing.
|
|
808
777
|
// Entered by opening the virtual `.trash` row. Each row keeps its node `id`
|
|
@@ -891,6 +860,20 @@ function wireParent(p: string): string {
|
|
|
891
860
|
return slash === -1 ? prefix : prefix + rel.slice(0, slash);
|
|
892
861
|
}
|
|
893
862
|
|
|
863
|
+
/* ui-fix — iki wire yolun AYNI dizini gösterip göstermediği (trailing-slash
|
|
864
|
+
* ve kök `adapter://` güvenli). Bir öğeyi ZATEN bulunduğu klasöre bırakma
|
|
865
|
+
* (source parent === target) no-op olmalı; aksi halde backend "kendine
|
|
866
|
+
* kopyala" 400'ü döner. */
|
|
867
|
+
function sameDir(a: string, b: string): boolean {
|
|
868
|
+
const norm = (s: string) => {
|
|
869
|
+
const i = s.indexOf('://');
|
|
870
|
+
const pre = i === -1 ? '' : s.slice(0, i + 3);
|
|
871
|
+
const rel = (i === -1 ? s : s.slice(i + 3)).replace(/\/+$/, '');
|
|
872
|
+
return pre + rel;
|
|
873
|
+
};
|
|
874
|
+
return norm(a) === norm(b);
|
|
875
|
+
}
|
|
876
|
+
|
|
894
877
|
function wireJoin(dir: string, name: string): string {
|
|
895
878
|
if (!dir) return name;
|
|
896
879
|
return dir.endsWith('://') || dir.endsWith('/') ? dir + name : `${dir}/${name}`;
|
|
@@ -1105,11 +1088,30 @@ async function openSearchHit(hit: GlobalSearchHit) {
|
|
|
1105
1088
|
|
|
1106
1089
|
useKeyboardShortcuts(rootEl, {
|
|
1107
1090
|
onDelete: () => {
|
|
1108
|
-
|
|
1091
|
+
/* ui-fix — kısayol da aktif panele gider (menüyle tutarlı). */
|
|
1092
|
+
if (paneIsActive.value) {
|
|
1093
|
+
const psel = splitPaneRef.value?.selectedNodes() ?? [];
|
|
1094
|
+
if (psel.length) {
|
|
1095
|
+
paneCtxTargets.value = psel;
|
|
1096
|
+
mutationInPane.value = true;
|
|
1097
|
+
showDelete.value = true;
|
|
1098
|
+
}
|
|
1099
|
+
} else if (!selection.isEmpty.value) {
|
|
1100
|
+
mutationInPane.value = false;
|
|
1101
|
+
showDelete.value = true;
|
|
1102
|
+
}
|
|
1109
1103
|
},
|
|
1110
1104
|
onRename: () => {
|
|
1111
|
-
if (
|
|
1105
|
+
if (paneIsActive.value) {
|
|
1106
|
+
const psel = splitPaneRef.value?.selectedNodes() ?? [];
|
|
1107
|
+
if (psel.length === 1) {
|
|
1108
|
+
renameTarget.value = psel[0];
|
|
1109
|
+
mutationInPane.value = true;
|
|
1110
|
+
showRename.value = true;
|
|
1111
|
+
}
|
|
1112
|
+
} else if (selection.nodes.value.length === 1) {
|
|
1112
1113
|
renameTarget.value = selection.nodes.value[0];
|
|
1114
|
+
mutationInPane.value = false;
|
|
1113
1115
|
showRename.value = true;
|
|
1114
1116
|
}
|
|
1115
1117
|
},
|
|
@@ -1390,61 +1392,19 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
|
|
|
1390
1392
|
ctxRef.value?.show({ clientX: payload.x, clientY: payload.y }, []);
|
|
1391
1393
|
}
|
|
1392
1394
|
|
|
1393
|
-
/* ui-fix — yan (ikincil) panelde sağ-tık: pane'i aktifleştir
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
* hepsi pane-route'lu (dispatchPaneAction). */
|
|
1395
|
+
/* ui-fix — yan (ikincil) panelde sağ-tık: pane'i aktifleştir + menüyü aç.
|
|
1396
|
+
* Menü ana panelle BİREBİR aynı (selectionActionList tek kaynak); aksiyonlar
|
|
1397
|
+
* dispatchItemAction'a gider ve ctxMode==='pane' iken pane-route'lanır. */
|
|
1397
1398
|
function onPaneContext(node: FileNode | null, ev: MouseEvent) {
|
|
1398
1399
|
activePane.value = 'split';
|
|
1399
1400
|
const sel = splitPaneRef.value?.selectedNodes() ?? [];
|
|
1400
|
-
// node=null (boş alana sağ-tık): seçimsiz menü (
|
|
1401
|
-
// halde pane seçimi (yoksa tıklanan node) hedeftir.
|
|
1401
|
+
// node=null (boş alana sağ-tık): seçimsiz menü (Yeni Klasör + Yapıştır).
|
|
1402
|
+
// Aksi halde pane seçimi (yoksa tıklanan node) hedeftir.
|
|
1402
1403
|
paneCtxTargets.value = node ? (sel.length > 0 ? sel : [node]) : [];
|
|
1403
1404
|
ctxMode.value = 'pane';
|
|
1404
1405
|
ctxRef.value?.show({ clientX: ev.clientX, clientY: ev.clientY }, paneCtxTargets.value);
|
|
1405
1406
|
}
|
|
1406
1407
|
|
|
1407
|
-
function paneActionList(sel: FileNode[]): ContextAction[] {
|
|
1408
|
-
const single = sel.length === 1;
|
|
1409
|
-
const isFile = single && sel[0]?.type === 'file';
|
|
1410
|
-
const isDir = single && sel[0]?.type === 'dir';
|
|
1411
|
-
const any = sel.length > 0;
|
|
1412
|
-
return [
|
|
1413
|
-
{ key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
|
|
1414
|
-
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !isDir } /* wiring:d1 */,
|
|
1415
|
-
{ key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
|
|
1416
|
-
{ divider: true, key: 'psep', label: '', hidden: !any },
|
|
1417
|
-
{ key: 'copy', label: t('ctx.copy'), icon: '❐', hidden: !any },
|
|
1418
|
-
{ key: 'cut', label: t('ctx.cut'), icon: '✂', hidden: !any },
|
|
1419
|
-
{ key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
|
|
1420
|
-
];
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
async function dispatchPaneAction(key: string, targets: FileNode[]) {
|
|
1424
|
-
switch (key) {
|
|
1425
|
-
case 'open':
|
|
1426
|
-
if (targets[0]) openNodeInNewTab(targets[0]);
|
|
1427
|
-
break;
|
|
1428
|
-
case 'open-tab':
|
|
1429
|
-
if (targets[0]) {
|
|
1430
|
-
tabsApi.openTab(paneToUser(targets[0].path), { viewMode: viewMode.value, background: true });
|
|
1431
|
-
}
|
|
1432
|
-
break;
|
|
1433
|
-
case 'download':
|
|
1434
|
-
if (targets[0]) downloadFile(targets[0]);
|
|
1435
|
-
break;
|
|
1436
|
-
case 'copy':
|
|
1437
|
-
paneCopy();
|
|
1438
|
-
break;
|
|
1439
|
-
case 'cut':
|
|
1440
|
-
paneCut();
|
|
1441
|
-
break;
|
|
1442
|
-
case 'paste':
|
|
1443
|
-
await panePaste();
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
1408
|
const contextActions = computed<ContextAction[]>(() => {
|
|
1449
1409
|
if (ctxMode.value === 'breadcrumb') {
|
|
1450
1410
|
return [
|
|
@@ -1452,8 +1412,16 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1452
1412
|
{ key: 'copy-path', label: t('breadcrumb.copy_path'), icon: '📋' },
|
|
1453
1413
|
];
|
|
1454
1414
|
}
|
|
1455
|
-
if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü */) {
|
|
1456
|
-
|
|
1415
|
+
if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü ana panelle BİREBİR aynı */) {
|
|
1416
|
+
const psel = paneCtxTargets.value;
|
|
1417
|
+
if (psel.length === 0) {
|
|
1418
|
+
// Boş alana sağ-tık: ana paneldeki canvas menüsüyle aynı.
|
|
1419
|
+
return [
|
|
1420
|
+
{ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' },
|
|
1421
|
+
{ key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
|
|
1422
|
+
];
|
|
1423
|
+
}
|
|
1424
|
+
return selectionActionList(psel);
|
|
1457
1425
|
}
|
|
1458
1426
|
|
|
1459
1427
|
const sel = selection.nodes.value;
|
|
@@ -1577,8 +1545,8 @@ async function onContextAction(action: ContextAction, targets: FileNode[]) {
|
|
|
1577
1545
|
}
|
|
1578
1546
|
return;
|
|
1579
1547
|
}
|
|
1580
|
-
if (ctxMode.value === 'pane' /* ui-fix — yan panel
|
|
1581
|
-
await
|
|
1548
|
+
if (ctxMode.value === 'pane' /* ui-fix — yan panel: aynı dispatch, pane-route */) {
|
|
1549
|
+
await dispatchItemAction(action.key, paneCtxTargets.value);
|
|
1582
1550
|
return;
|
|
1583
1551
|
}
|
|
1584
1552
|
await dispatchItemAction(action.key, targets);
|
|
@@ -1633,30 +1601,40 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1633
1601
|
case 'rename':
|
|
1634
1602
|
if (targets[0]) {
|
|
1635
1603
|
renameTarget.value = targets[0];
|
|
1604
|
+
mutationInPane.value = ctxMode.value === 'pane'; /* ui-fix */
|
|
1636
1605
|
showRename.value = true;
|
|
1637
1606
|
}
|
|
1638
1607
|
break;
|
|
1639
1608
|
case 'cut':
|
|
1640
|
-
|
|
1641
|
-
|
|
1609
|
+
/* ui-fix — pane bağlamında pano kaynağı pane'in dizini olmalı. */
|
|
1610
|
+
if (ctxMode.value === 'pane') paneCut();
|
|
1611
|
+
else {
|
|
1612
|
+
clipboard.value = { mode: 'cut', items: targets, sourcePath: currentPath.value };
|
|
1613
|
+
flashToast('Kes → Yapıştır hazır');
|
|
1614
|
+
}
|
|
1642
1615
|
break;
|
|
1643
1616
|
case 'copy':
|
|
1644
|
-
|
|
1645
|
-
|
|
1617
|
+
if (ctxMode.value === 'pane') paneCopy();
|
|
1618
|
+
else {
|
|
1619
|
+
clipboard.value = { mode: 'copy', items: targets, sourcePath: currentPath.value };
|
|
1620
|
+
flashToast('Kopyala → Yapıştır hazır');
|
|
1621
|
+
}
|
|
1646
1622
|
break;
|
|
1647
1623
|
case 'paste':
|
|
1648
1624
|
/* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
|
|
1649
1625
|
* (klavye kısayolu zaten pane-route'luydu; menü değildi). */
|
|
1650
|
-
if (paneIsActive.value) await panePaste();
|
|
1626
|
+
if (ctxMode.value === 'pane' || paneIsActive.value) await panePaste();
|
|
1651
1627
|
else await paste();
|
|
1652
1628
|
break;
|
|
1653
1629
|
case 'delete':
|
|
1630
|
+
mutationInPane.value = ctxMode.value === 'pane'; /* ui-fix */
|
|
1654
1631
|
showDelete.value = true;
|
|
1655
1632
|
break;
|
|
1656
1633
|
case 'restore':
|
|
1657
1634
|
if (targets.length > 0) await restoreSelection(targets);
|
|
1658
1635
|
break;
|
|
1659
1636
|
case 'new-folder':
|
|
1637
|
+
mutationInPane.value = ctxMode.value === 'pane'; /* ui-fix */
|
|
1660
1638
|
showNewFolder.value = true;
|
|
1661
1639
|
break;
|
|
1662
1640
|
case 'duplicate':
|
|
@@ -1739,10 +1717,13 @@ function downloadFile(n: FileNode) {
|
|
|
1739
1717
|
// ------- Modals -------
|
|
1740
1718
|
|
|
1741
1719
|
async function submitNewFolder(name: string) {
|
|
1720
|
+
const inPane = mutationInPane.value; /* ui-fix — yan panelde yeni klasör */
|
|
1742
1721
|
try {
|
|
1743
|
-
|
|
1722
|
+
const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
|
|
1723
|
+
await api.newFolder(dirWire, name);
|
|
1744
1724
|
showNewFolder.value = false;
|
|
1745
|
-
await
|
|
1725
|
+
if (inPane) await splitPaneRef.value?.reload();
|
|
1726
|
+
else await load();
|
|
1746
1727
|
} catch (err) {
|
|
1747
1728
|
emit('error', { message: (err as Error).message, context: { op: 'newfolder' } });
|
|
1748
1729
|
}
|
|
@@ -1751,14 +1732,16 @@ async function submitNewFolder(name: string) {
|
|
|
1751
1732
|
async function submitRename(name: string) {
|
|
1752
1733
|
const target = renameTarget.value;
|
|
1753
1734
|
if (!target) return;
|
|
1735
|
+
const inPane = mutationInPane.value; /* ui-fix — yan panelden yeniden adlandırma */
|
|
1754
1736
|
try {
|
|
1755
|
-
const dirWire = qualify(currentPath.value);
|
|
1737
|
+
const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
|
|
1756
1738
|
const oldPath = target.path; // qualified
|
|
1757
1739
|
const oldName = target.basename;
|
|
1758
1740
|
await api.rename(dirWire, oldPath, name);
|
|
1759
1741
|
showRename.value = false;
|
|
1760
1742
|
renameTarget.value = null;
|
|
1761
|
-
await
|
|
1743
|
+
if (inPane) await splitPaneRef.value?.reload();
|
|
1744
|
+
else await load();
|
|
1762
1745
|
// Clean inverse: rename the new path back to the old basename.
|
|
1763
1746
|
if (name && name !== oldName) {
|
|
1764
1747
|
const newPath = wireJoin(wireParent(oldPath), name);
|
|
@@ -1780,7 +1763,10 @@ async function confirmDelete() {
|
|
|
1780
1763
|
flashToast('Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.');
|
|
1781
1764
|
return;
|
|
1782
1765
|
}
|
|
1783
|
-
|
|
1766
|
+
/* ui-fix — yan panelden silme: hedefler + dizin + tazeleme pane'e ait. */
|
|
1767
|
+
const inPane = mutationInPane.value;
|
|
1768
|
+
const targets = inPane ? paneCtxTargets.value : selection.nodes.value;
|
|
1769
|
+
const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
|
|
1784
1770
|
const items = targets.map((n) => n.path);
|
|
1785
1771
|
if (items.length === 0) {
|
|
1786
1772
|
showDelete.value = false;
|
|
@@ -1801,19 +1787,21 @@ async function confirmDelete() {
|
|
|
1801
1787
|
: null;
|
|
1802
1788
|
try {
|
|
1803
1789
|
if (api.endpoints.deleteAsync) {
|
|
1804
|
-
const { op } = await api.deleteAsync(items,
|
|
1790
|
+
const { op } = await api.deleteAsync(items, dirWire);
|
|
1805
1791
|
if (restoreUndo) {
|
|
1806
1792
|
opUndo.set(op.id, { message: t('toast.trashed'), fn: restoreUndo });
|
|
1807
1793
|
}
|
|
1808
1794
|
pendingOps.register(op);
|
|
1809
1795
|
flashToast('Silme kuyruğa alındı');
|
|
1810
1796
|
} else {
|
|
1811
|
-
await api.deleteItems(
|
|
1812
|
-
await
|
|
1797
|
+
await api.deleteItems(dirWire, items);
|
|
1798
|
+
if (inPane) await splitPaneRef.value?.reload();
|
|
1799
|
+
else await load();
|
|
1813
1800
|
if (restoreUndo) undoToast(t('toast.trashed'), restoreUndo);
|
|
1814
1801
|
}
|
|
1815
1802
|
showDelete.value = false;
|
|
1816
|
-
|
|
1803
|
+
if (inPane) void splitPaneRef.value?.reload();
|
|
1804
|
+
else selection.clear();
|
|
1817
1805
|
} catch (err) {
|
|
1818
1806
|
emit('error', { message: (err as Error).message, context: { op: 'delete' } });
|
|
1819
1807
|
}
|
|
@@ -2180,11 +2168,10 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
|
2180
2168
|
const targetDir = target.path; // qualified
|
|
2181
2169
|
const sources = items
|
|
2182
2170
|
.map((i) => i.path)
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
}
|
|
2171
|
+
// ui-fix — öğe zaten targetDir'in içindeyse (parent===target) atla: yerinde
|
|
2172
|
+
// bırakma no-op, backend "kendine kopyala" 400'ü tetiklemez.
|
|
2173
|
+
.filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
|
|
2174
|
+
if (sources.length === 0) return; // sessiz no-op (yerinde bırakma)
|
|
2188
2175
|
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2189
2176
|
}
|
|
2190
2177
|
|
|
@@ -2202,7 +2189,8 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
|
2202
2189
|
const targetDir = adapterPath; // already qualified by breadcrumb
|
|
2203
2190
|
const sources = items
|
|
2204
2191
|
.map((i) => i.path)
|
|
2205
|
-
|
|
2192
|
+
// ui-fix — kırıntıya (aynı klasöre) yerinde bırakma no-op.
|
|
2193
|
+
.filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
|
|
2206
2194
|
if (sources.length === 0) return;
|
|
2207
2195
|
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2208
2196
|
}
|
|
@@ -2594,6 +2582,12 @@ function closeSplit() {
|
|
|
2594
2582
|
function onPaneNavigate(p: string) {
|
|
2595
2583
|
tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
|
|
2596
2584
|
}
|
|
2585
|
+
/* ui-fix — çöp kutusu satırı yan panelde açılınca: trash görünümü (geri
|
|
2586
|
+
* yükleme aksiyonlarıyla) ana panele aittir → ana paneli aktif edip aç. */
|
|
2587
|
+
function onPaneOpenTrash() {
|
|
2588
|
+
activePane.value = 'main';
|
|
2589
|
+
void loadTrash();
|
|
2590
|
+
}
|
|
2597
2591
|
/* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
|
|
2598
2592
|
* devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
|
|
2599
2593
|
* toggle'ı AKTİF panele yazar (Burak: "B tıklıyken ikon değiştir dersem
|
|
@@ -2686,7 +2680,11 @@ async function transferItems(
|
|
|
2686
2680
|
originWire?: string,
|
|
2687
2681
|
forceCopy = false,
|
|
2688
2682
|
): Promise<void> {
|
|
2689
|
-
|
|
2683
|
+
// ui-fix — yerinde bırakma (source parent === target) no-op: backend
|
|
2684
|
+
// "kendine kopyala" 400'ü engellenir (paneller arası + pano yolu).
|
|
2685
|
+
const list = sources.filter(
|
|
2686
|
+
(p) => p && p !== targetWire && !targetWire.startsWith(p + '/') && !sameDir(wireParent(p), targetWire),
|
|
2687
|
+
);
|
|
2690
2688
|
if (list.length === 0 || !targetWire) return;
|
|
2691
2689
|
const targetAdapter = wireAdapterOf(targetWire);
|
|
2692
2690
|
const cross = list.some((p) => wireAdapterOf(p) !== targetAdapter);
|
|
@@ -3323,12 +3321,14 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3323
3321
|
:active="paneIsActive"
|
|
3324
3322
|
:view-mode="paneViewMode /* ui-fix */"
|
|
3325
3323
|
:thumb-src="thumbs.src /* ui-fix */"
|
|
3324
|
+
:trash-visible="config.trashVisible !== false /* ui-fix — çöp satırı simetrisi */"
|
|
3326
3325
|
@navigate="onPaneNavigate"
|
|
3327
3326
|
@activate="activePane = 'split'"
|
|
3328
3327
|
@close="closeSplit"
|
|
3329
3328
|
@open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
|
|
3330
3329
|
@transfer="onPaneTransfer"
|
|
3331
3330
|
@context="onPaneContext /* ui-fix — yan panel sağ-tık menüsü */"
|
|
3331
|
+
@open-trash="onPaneOpenTrash /* ui-fix — çöp kutusu ana panelde açılır */"
|
|
3332
3332
|
/>
|
|
3333
3333
|
<!-- /wiring:d1 -->
|
|
3334
3334
|
|
|
@@ -21,6 +21,7 @@ import type { FileApi } from '../composables/useFileApi';
|
|
|
21
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 { filterInternalEntries, injectTrashRow, hydrateTrashRow } from '../lib/listing';
|
|
24
25
|
import ListView from './ListView.vue';
|
|
25
26
|
import GridView from './GridView.vue';
|
|
26
27
|
import GalleryView from './GalleryView.vue';
|
|
@@ -57,6 +58,9 @@ const props = defineProps<{
|
|
|
57
58
|
viewMode?: ViewMode;
|
|
58
59
|
/** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
|
|
59
60
|
thumbSrc?: (n: FileNode) => string | null;
|
|
61
|
+
/** ui-fix — mirror the main panel's virtual `.trash` row at storage root
|
|
62
|
+
* so both split panes list identical rows (no row-offset). Defaults on. */
|
|
63
|
+
trashVisible?: boolean;
|
|
60
64
|
}>();
|
|
61
65
|
|
|
62
66
|
const emit = defineEmits<{
|
|
@@ -68,6 +72,9 @@ const emit = defineEmits<{
|
|
|
68
72
|
/* ui-fix — yan panelde sağ-tık: menü ana bileşende (FileExplorer) açılır.
|
|
69
73
|
* node=null → boş alana sağ-tık (seçimsiz, yalnız Yapıştır). */
|
|
70
74
|
(e: 'context', node: FileNode | null, ev: MouseEvent): void;
|
|
75
|
+
/* ui-fix — çöp kutusu satırı açıldığında: trash görünümü ana panele aittir
|
|
76
|
+
* (geri yükleme aksiyonlarıyla), host loadTrash() ile açar. */
|
|
77
|
+
(e: 'open-trash'): void;
|
|
71
78
|
}>();
|
|
72
79
|
|
|
73
80
|
const { t } = useLocale(() => props.locale);
|
|
@@ -101,14 +108,12 @@ async function loadPane(target?: string): Promise<void> {
|
|
|
101
108
|
return;
|
|
102
109
|
}
|
|
103
110
|
const resp = await props.api.index(props.qualify(requested));
|
|
104
|
-
// Same internal-entry filter as the main
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return true;
|
|
111
|
-
});
|
|
111
|
+
// Same internal-entry filter + virtual `.trash` row as the main panel
|
|
112
|
+
// (shared helpers → both split panes list identical rows, no offset).
|
|
113
|
+
files.value = filterInternalEntries(resp.files);
|
|
114
|
+
if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.trashVisible !== false)) {
|
|
115
|
+
void hydrateTrashRow(files.value, resp.adapter, props.api);
|
|
116
|
+
}
|
|
112
117
|
path.value = props.toUser(resp.dirname);
|
|
113
118
|
selected.value = new Set();
|
|
114
119
|
emit('navigate', path.value);
|
|
@@ -204,6 +209,13 @@ function onViewDropInto(target: FileNode, ev: DragEvent) {
|
|
|
204
209
|
|
|
205
210
|
function onRowDbl(n: FileNode) {
|
|
206
211
|
if (n.type !== 'dir') return;
|
|
212
|
+
// The virtual `.trash` row opens the backend trash listing (restore
|
|
213
|
+
// actions) — that view lives in the main panel; let the host open it.
|
|
214
|
+
if (n.basename === '.trash') {
|
|
215
|
+
emit('activate');
|
|
216
|
+
emit('open-trash');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
207
219
|
void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
|
|
208
220
|
}
|
|
209
221
|
|
|
@@ -217,7 +229,7 @@ function clearSelection() {
|
|
|
217
229
|
|
|
218
230
|
function onRowDragStart(n: FileNode, ev: DragEvent) {
|
|
219
231
|
if (!ev.dataTransfer) return;
|
|
220
|
-
if (isStorageRow(n) || atVirtualRoot.value) {
|
|
232
|
+
if (isStorageRow(n) || atVirtualRoot.value || n.basename === '.trash') {
|
|
221
233
|
ev.preventDefault();
|
|
222
234
|
return;
|
|
223
235
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* listing.ts — shared listing helpers used by BOTH the main panel
|
|
3
|
+
* (FileExplorer.load) and the split-view secondary panel
|
|
4
|
+
* (SecondaryPane.loadPane). SINGLE SOURCE so the two panes can never
|
|
5
|
+
* drift apart: the internal-entry filter and the virtual `.trash` row
|
|
6
|
+
* must be identical in both, otherwise split view shows mismatched rows
|
|
7
|
+
* (the trash row missing on one side → visible row-offset).
|
|
8
|
+
*/
|
|
9
|
+
import type { FileNode } from '../types/FileNode';
|
|
10
|
+
import { E2E_MARKER_NAME } from './e2ecrypto';
|
|
11
|
+
|
|
12
|
+
/** Adapter-strip: `s3-test://fileman/x` → `fileman/x`. */
|
|
13
|
+
export function stripAdapter(p: string): string {
|
|
14
|
+
const idx = p.indexOf('://');
|
|
15
|
+
return idx === -1 ? p : p.slice(idx + 3);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hide system/internal entries the user must never see as files:
|
|
20
|
+
* thumbnails, version history, the soft-delete store, keepdir markers
|
|
21
|
+
* and the E2E marker. Shared by both panes' listing filters.
|
|
22
|
+
*/
|
|
23
|
+
export function filterInternalEntries(files: FileNode[]): FileNode[] {
|
|
24
|
+
return (files || []).filter((f) => {
|
|
25
|
+
if (f.path.includes('.thumbs')) return false;
|
|
26
|
+
if (f.path.includes('.versions') || f.basename === '.versions') return false;
|
|
27
|
+
if (f.basename === '.trash') return false;
|
|
28
|
+
if (f.basename === '.keepdir') return false;
|
|
29
|
+
if (f.basename === E2E_MARKER_NAME) return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when `dirname` (adapter-qualified) is the storage root, where the
|
|
35
|
+
* virtual trash row belongs. */
|
|
36
|
+
export function isStorageRootDir(dirname: string): boolean {
|
|
37
|
+
const rel = stripAdapter(dirname);
|
|
38
|
+
return rel === 'fileman' || rel === '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when the listing IS the trash view (don't inject the row into itself). */
|
|
42
|
+
export function isTrashListing(dirname: string): boolean {
|
|
43
|
+
return stripAdapter(dirname).startsWith('fileman/.trash');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The synthetic `.trash` row shown at storage root — rendered as
|
|
47
|
+
* "Çöp Kutusu" / "Trash" via the `.trash` basename → locale mapping. */
|
|
48
|
+
export function makeTrashRow(adapter: string): FileNode {
|
|
49
|
+
return {
|
|
50
|
+
type: 'dir',
|
|
51
|
+
path: `${adapter}://fileman/.trash`,
|
|
52
|
+
basename: '.trash',
|
|
53
|
+
extension: '',
|
|
54
|
+
storage: adapter,
|
|
55
|
+
visibility: 'private',
|
|
56
|
+
size: 0,
|
|
57
|
+
file_size: 0,
|
|
58
|
+
mime_type: 'inode/directory',
|
|
59
|
+
extra_metadata: {},
|
|
60
|
+
} as unknown as FileNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Inject the virtual `.trash` row at the front of a root listing when
|
|
65
|
+
* enabled. Returns true if a row was added (so the caller can hydrate
|
|
66
|
+
* it). Mutates `files` in place. Single source for both panes.
|
|
67
|
+
*/
|
|
68
|
+
export function injectTrashRow(
|
|
69
|
+
files: FileNode[],
|
|
70
|
+
adapter: string,
|
|
71
|
+
dirname: string,
|
|
72
|
+
trashVisible: boolean,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!trashVisible) return false;
|
|
75
|
+
if (isTrashListing(dirname)) return false;
|
|
76
|
+
if (!isStorageRootDir(dirname)) return false;
|
|
77
|
+
files.unshift(makeTrashRow(adapter));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Best-effort fill of the trash row's size (total bytes) + date (newest
|
|
82
|
+
* deletion) from the backend trash listing, so it reads like a real
|
|
83
|
+
* folder instead of "— / —". Non-blocking; mutates the row in place. */
|
|
84
|
+
export async function hydrateTrashRow(
|
|
85
|
+
files: FileNode[],
|
|
86
|
+
storage: string,
|
|
87
|
+
api: { listTrash: (s?: string) => Promise<{ entries: Array<{ size?: number; deleted_at: string }> }> },
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
try {
|
|
90
|
+
const { entries } = await api.listTrash(storage);
|
|
91
|
+
const row = files.find((f) => f.basename === '.trash');
|
|
92
|
+
if (!row) return;
|
|
93
|
+
let total = 0;
|
|
94
|
+
let newest = 0;
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
total += e.size || 0;
|
|
97
|
+
const ts = Date.parse(e.deleted_at);
|
|
98
|
+
if (!Number.isNaN(ts) && ts > newest) newest = ts;
|
|
99
|
+
}
|
|
100
|
+
row.size = total;
|
|
101
|
+
if (newest > 0) row.last_modified = newest;
|
|
102
|
+
} catch {
|
|
103
|
+
/* keep the bare row */
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/styles/base.css
CHANGED
|
@@ -266,6 +266,9 @@ filex-explorer {
|
|
|
266
266
|
gap: 4px;
|
|
267
267
|
flex-wrap: wrap;
|
|
268
268
|
padding: 6px 12px;
|
|
269
|
+
/* ui-fix — split'te yan panel .fe-split__crumbs ile aynı yükseklik (ana
|
|
270
|
+
panelin "Yola Git" ✏ butonu yoksa da satırlar aynı y'den başlasın). */
|
|
271
|
+
min-height: 37px;
|
|
269
272
|
background: var(--fe-bg);
|
|
270
273
|
border-bottom: 1px solid var(--fe-border);
|
|
271
274
|
font-size: 13px;
|
|
@@ -3262,11 +3265,14 @@ filex-explorer {
|
|
|
3262
3265
|
.fe-split__crumbs {
|
|
3263
3266
|
display: flex;
|
|
3264
3267
|
align-items: center;
|
|
3265
|
-
gap
|
|
3268
|
+
/* ui-fix — ana panel .fe-breadcrumb ile BİREBİR aynı ölçüler (gap/padding/
|
|
3269
|
+
font) → split'te iki breadcrumb tam hizalı, ince kayma yok. */
|
|
3270
|
+
gap: 4px;
|
|
3266
3271
|
flex: 0 0 auto;
|
|
3267
|
-
padding: 6px
|
|
3272
|
+
padding: 6px 12px;
|
|
3273
|
+
min-height: 37px; /* ui-fix — ana panel .fe-breadcrumb ile aynı yükseklik */
|
|
3268
3274
|
border-bottom: 1px solid var(--fe-border);
|
|
3269
|
-
font-size:
|
|
3275
|
+
font-size: 13px;
|
|
3270
3276
|
color: var(--fe-text-muted);
|
|
3271
3277
|
overflow: hidden;
|
|
3272
3278
|
white-space: nowrap;
|
|
@@ -3276,8 +3282,8 @@ filex-explorer {
|
|
|
3276
3282
|
background: transparent;
|
|
3277
3283
|
color: inherit;
|
|
3278
3284
|
font: inherit;
|
|
3279
|
-
padding: 2px
|
|
3280
|
-
border-radius:
|
|
3285
|
+
padding: 2px 6px; /* ui-fix — .fe-breadcrumb__crumb ile aynı */
|
|
3286
|
+
border-radius: var(--fe-radius-sm);
|
|
3281
3287
|
cursor: pointer;
|
|
3282
3288
|
max-width: 140px;
|
|
3283
3289
|
overflow: hidden;
|