@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.
Files changed (31) hide show
  1. package/dist/{ArchiveViewer-DAb5peBU.js → ArchiveViewer-BRFvhWwL.js} +2 -2
  2. package/dist/{ArchiveViewer-DAb5peBU.js.map → ArchiveViewer-BRFvhWwL.js.map} +1 -1
  3. package/dist/{CsvViewer-_bnmnpjn.js → CsvViewer-1cGGKeGi.js} +2 -2
  4. package/dist/{CsvViewer-_bnmnpjn.js.map → CsvViewer-1cGGKeGi.js.map} +1 -1
  5. package/dist/{DrawioViewer-Bjns4ob8.js → DrawioViewer-C0oThT59.js} +2 -2
  6. package/dist/{DrawioViewer-Bjns4ob8.js.map → DrawioViewer-C0oThT59.js.map} +1 -1
  7. package/dist/{EpubViewer-DoFuZTtA.js → EpubViewer-DCPOD2S-.js} +2 -2
  8. package/dist/{EpubViewer-DoFuZTtA.js.map → EpubViewer-DCPOD2S-.js.map} +1 -1
  9. package/dist/{IpynbViewer-eg1ySMD4.js → IpynbViewer-mv4xiv3H.js} +2 -2
  10. package/dist/{IpynbViewer-eg1ySMD4.js.map → IpynbViewer-mv4xiv3H.js.map} +1 -1
  11. package/dist/{MermaidViewer-BZ13vA6S.js → MermaidViewer-BBNVghTz.js} +2 -2
  12. package/dist/{MermaidViewer-BZ13vA6S.js.map → MermaidViewer-BBNVghTz.js.map} +1 -1
  13. package/dist/{PsdViewer-qFkE4KCB.js → PsdViewer-B8ygWw-s.js} +2 -2
  14. package/dist/{PsdViewer-qFkE4KCB.js.map → PsdViewer-B8ygWw-s.js.map} +1 -1
  15. package/dist/{TiffViewer-eHJTCYsd.js → TiffViewer-BhHjf3s5.js} +2 -2
  16. package/dist/{TiffViewer-eHJTCYsd.js.map → TiffViewer-BhHjf3s5.js.map} +1 -1
  17. package/dist/{Viewer3D-C4--ljdO.js → Viewer3D-BhOJpzyG.js} +2 -2
  18. package/dist/{Viewer3D-C4--ljdO.js.map → Viewer3D-BhOJpzyG.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +34 -34
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-BSDT0PU8.js → index-BjiUhk9K.js} +4008 -3989
  23. package/dist/index-BjiUhk9K.js.map +1 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +123 -123
  28. package/src/components/SecondaryPane.vue +21 -9
  29. package/src/lib/listing.ts +105 -0
  30. package/src/styles/base.css +11 -5
  31. package/dist/index-BSDT0PU8.js.map +0 -1
@@ -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 || []).filter((f) => {
708
- if (f.path.includes('.thumbs')) return false;
709
- if (f.path.includes('.versions') || f.basename === '.versions') return false;
710
- if (f.basename === '.trash') return false;
711
- if (f.basename === '.keepdir') return false;
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 the virtual `.trash` row is synthesized with no size/date;
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
- if (!selection.isEmpty.value) showDelete.value = true;
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 (selection.nodes.value.length === 1) {
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, seçili
1394
- * node'lara göre menüyü aç. Pane "hafif" olduğundan (delete/rename modal'ları
1395
- * ana panelde) menü aç/yeni-sekme/indir/kopyala/kes/yapıştır ile sınırlı;
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ü (yalnız Yapıştır). Aksi
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
- return paneActionList(paneCtxTargets.value);
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 aksiyonları pane-route */) {
1581
- await dispatchPaneAction(action.key, paneCtxTargets.value);
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
- clipboard.value = { mode: 'cut', items: targets, sourcePath: currentPath.value };
1641
- flashToast('Kes Yapıştır hazır');
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
- clipboard.value = { mode: 'copy', items: targets, sourcePath: currentPath.value };
1645
- flashToast('Kopyala → Yapıştır hazır');
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
- await api.newFolder(qualify(currentPath.value), name);
1722
+ const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
1723
+ await api.newFolder(dirWire, name);
1744
1724
  showNewFolder.value = false;
1745
- await load();
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 load();
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
- const targets = selection.nodes.value;
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, qualify(currentPath.value));
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(qualify(currentPath.value), items);
1812
- await load();
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
- selection.clear();
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
- .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
2184
- if (sources.length === 0) {
2185
- flashToast('Aynı klasöre taşınamaz');
2186
- return;
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
- .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
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
- const list = sources.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
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 listing.
105
- files.value = (resp.files || []).filter((f) => {
106
- if (f.path.includes('.thumbs')) return false;
107
- if (f.path.includes('.versions') || f.basename === '.versions') return false;
108
- if (f.basename === '.trash') return false;
109
- if (f.basename === '.keepdir') return false;
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
+ }
@@ -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: 2px;
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 8px;
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: 12px;
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 5px;
3280
- border-radius: 5px;
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;