@brftech/filex-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArchiveViewer-DKW-HyXT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
- package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
- package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-BvriJ42M.js} +2 -2
- package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
- package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-B_bSPsWX.js} +2 -2
- package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
- package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-hl68h4gG.js} +2 -2
- package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
- package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-lhwOjXK7.js} +2 -2
- package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
- package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-H2Z6AYXg.js} +2 -2
- package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
- package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-Bsk-11yH.js} +2 -2
- package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
- package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-D-j9vycG.js} +2 -2
- package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
- package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-Bcr70rTe.js} +2 -2
- package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
- package/dist/filex-core.js +42 -39
- package/dist/filex-core.umd.cjs +59 -58
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-C_ZSnk_I.js +11109 -0
- package/dist/index-C_ZSnk_I.js.map +1 -0
- package/dist/index.d.ts +234 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +367 -14
- package/src/components/CommandPalette.vue +12 -1
- package/src/components/GalleryView.vue +214 -0
- package/src/components/GridView.vue +1 -0
- package/src/components/InspectorPanel.vue +170 -0
- package/src/components/ListView.vue +1 -0
- package/src/components/SecondaryPane.vue +411 -0
- package/src/components/TabBar.vue +174 -0
- package/src/components/Toolbar.vue +23 -5
- package/src/composables/useFileApi.ts +53 -0
- package/src/composables/useKeyboardShortcuts.ts +19 -0
- package/src/composables/useTabs.ts +222 -0
- package/src/index.ts +6 -0
- package/src/locales/en.ts +33 -0
- package/src/locales/tr.ts +33 -0
- package/src/styles/base.css +538 -0
- package/src/types/FileNode.ts +1 -1
- package/dist/index-eARevpz0.js +0 -9952
- package/dist/index-eARevpz0.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -42,6 +42,7 @@ import RecentlyOpened from './components/RecentlyOpened.vue';
|
|
|
42
42
|
import Breadcrumb from './components/Breadcrumb.vue';
|
|
43
43
|
import ListView from './components/ListView.vue';
|
|
44
44
|
import GridView from './components/GridView.vue';
|
|
45
|
+
import GalleryView from './components/GalleryView.vue'; /* wiring:d2 */
|
|
45
46
|
import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
|
|
46
47
|
import UploadProgress from './components/UploadProgress.vue';
|
|
47
48
|
import PendingOpsTray from './components/PendingOpsTray.vue';
|
|
@@ -65,6 +66,11 @@ import { useOperations } from './composables/useOperations';
|
|
|
65
66
|
/* wiring:c4 */
|
|
66
67
|
import OnboardingTour from './components/OnboardingTour.vue';
|
|
67
68
|
/* /wiring:c4 */
|
|
69
|
+
/* wiring:d1 — sekmeler + tab başına split */
|
|
70
|
+
import TabBar from './components/TabBar.vue';
|
|
71
|
+
import SecondaryPane from './components/SecondaryPane.vue';
|
|
72
|
+
import { useTabs, type TabState } from './composables/useTabs';
|
|
73
|
+
/* /wiring:d1 */
|
|
68
74
|
|
|
69
75
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
70
76
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -167,6 +173,7 @@ const pendingOps = usePendingOps(props.config, api, {
|
|
|
167
173
|
flashToast(`${verb} (${op.progress_total})`);
|
|
168
174
|
}
|
|
169
175
|
void load();
|
|
176
|
+
void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
|
|
170
177
|
},
|
|
171
178
|
});
|
|
172
179
|
|
|
@@ -206,7 +213,7 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
|
|
|
206
213
|
let value: ViewMode = (() => {
|
|
207
214
|
try {
|
|
208
215
|
const stored = localStorage.getItem(VIEW_MODE_KEY);
|
|
209
|
-
if (stored === 'list' || stored === 'grid') return stored;
|
|
216
|
+
if (stored === 'list' || stored === 'grid' || stored === 'gallery') return stored; /* wiring:d2 */
|
|
210
217
|
} catch {
|
|
211
218
|
/* private mode */
|
|
212
219
|
}
|
|
@@ -1084,8 +1091,9 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1084
1091
|
showRename.value = true;
|
|
1085
1092
|
}
|
|
1086
1093
|
},
|
|
1087
|
-
onSelectAll: () => selection.selectAll()
|
|
1094
|
+
onSelectAll: () => (paneIsActive.value ? splitPaneRef.value?.selectAll() : selection.selectAll()) /* wiring:d1 pane-route */,
|
|
1088
1095
|
onOpen: () => {
|
|
1096
|
+
if (paneIsActive.value) return splitPaneRef.value?.openSelected(); /* wiring:d1 pane-route */
|
|
1089
1097
|
const n = selection.nodes.value[0];
|
|
1090
1098
|
if (n) openNode(n);
|
|
1091
1099
|
},
|
|
@@ -1102,10 +1110,10 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1102
1110
|
if (isNarrow.value) closeInspector();
|
|
1103
1111
|
},
|
|
1104
1112
|
onFocusSearch: () => toolbarRef.value?.focusSearch(),
|
|
1105
|
-
onCut: () => cut()
|
|
1106
|
-
onCopy: () => copyToClipboard()
|
|
1107
|
-
onPaste: () => paste()
|
|
1108
|
-
onGoUp: () => goUp()
|
|
1113
|
+
onCut: () => (paneIsActive.value ? paneCut() : cut()) /* wiring:d1 pane-route */,
|
|
1114
|
+
onCopy: () => (paneIsActive.value ? paneCopy() : copyToClipboard()) /* wiring:d1 pane-route */,
|
|
1115
|
+
onPaste: () => (paneIsActive.value ? void panePaste() : void paste()) /* wiring:d1 pane-route */,
|
|
1116
|
+
onGoUp: () => (paneIsActive.value ? splitPaneRef.value?.goUp() : goUp()) /* wiring:d1 pane-route */,
|
|
1109
1117
|
/* cila:c wiring */
|
|
1110
1118
|
onPathJump: () => {
|
|
1111
1119
|
showPalette.value = true;
|
|
@@ -1116,6 +1124,12 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1116
1124
|
/* /cila:c wiring */
|
|
1117
1125
|
onQuickLook: () => quickLookToggle() /* wiring:c2 */,
|
|
1118
1126
|
onToggleInspector: () => toggleInspector() /* koru:k1 */,
|
|
1127
|
+
/* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
|
|
1128
|
+
onTabNew: () => newTabHere(),
|
|
1129
|
+
onTabClose: () => closeTabById(tabsActiveId.value),
|
|
1130
|
+
onTabNext: () => nextTab(),
|
|
1131
|
+
onTabPrev: () => prevTab(),
|
|
1132
|
+
/* /wiring:d1 */
|
|
1119
1133
|
hasSelection: () => !selection.isEmpty.value,
|
|
1120
1134
|
});
|
|
1121
1135
|
|
|
@@ -1370,6 +1384,7 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1370
1384
|
if (!single) return [];
|
|
1371
1385
|
return [
|
|
1372
1386
|
{ key: 'open', label: t('ctx.open'), icon: '↗' },
|
|
1387
|
+
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
|
|
1373
1388
|
];
|
|
1374
1389
|
}
|
|
1375
1390
|
|
|
@@ -1410,6 +1425,7 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
|
|
|
1410
1425
|
const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
|
|
1411
1426
|
return [
|
|
1412
1427
|
{ key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
|
|
1428
|
+
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede aç */,
|
|
1413
1429
|
{ key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
|
|
1414
1430
|
{ key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
|
|
1415
1431
|
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w, disabled: !isFile },
|
|
@@ -1535,6 +1551,11 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1535
1551
|
case 'duplicate':
|
|
1536
1552
|
if (targets[0]) await duplicate(targets[0]);
|
|
1537
1553
|
break;
|
|
1554
|
+
/* wiring:d1 — sağ-tık "Yeni sekmede aç" */
|
|
1555
|
+
case 'open-tab':
|
|
1556
|
+
if (targets[0]) openNodeInTab(targets[0]);
|
|
1557
|
+
break;
|
|
1558
|
+
/* /wiring:d1 */
|
|
1538
1559
|
}
|
|
1539
1560
|
}
|
|
1540
1561
|
|
|
@@ -1881,11 +1902,38 @@ function onDragLeave() {
|
|
|
1881
1902
|
if (dragCounter.value === 0) dragOver.value = false;
|
|
1882
1903
|
}
|
|
1883
1904
|
function onDragOver(ev: DragEvent) {
|
|
1905
|
+
/* wiring:d1 — iç sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
|
|
1906
|
+
panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
|
|
1907
|
+
okunamaz — karar drop anında verilir; aynı-klasör drop'u no-op kalır). */
|
|
1908
|
+
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
1909
|
+
ev.preventDefault();
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
/* /wiring:d1 */
|
|
1884
1913
|
if (isExternalFileDrag(ev)) {
|
|
1885
1914
|
ev.preventDefault();
|
|
1886
1915
|
}
|
|
1887
1916
|
}
|
|
1888
1917
|
function onDropUpload(ev: DragEvent) {
|
|
1918
|
+
/* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
|
|
1919
|
+
klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
|
|
1920
|
+
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
1921
|
+
const d1Origin = ev.dataTransfer.getData(FE_DND_SRC_MIME) || '';
|
|
1922
|
+
const d1Here = qualify(currentPath.value);
|
|
1923
|
+
if (d1Origin && d1Here && d1Origin !== d1Here && !trashMode.value && canWriteHere.value) {
|
|
1924
|
+
ev.preventDefault();
|
|
1925
|
+
dragCounter.value = 0;
|
|
1926
|
+
dragOver.value = false;
|
|
1927
|
+
try {
|
|
1928
|
+
const d1Items = JSON.parse(ev.dataTransfer.getData(FE_DND_MIME)) as Array<{ path: string }>;
|
|
1929
|
+
void transferItems(d1Items.map((i) => i.path), d1Here, d1Origin);
|
|
1930
|
+
} catch {
|
|
1931
|
+
/* bozuk payload — yok say */
|
|
1932
|
+
}
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/* /wiring:d1 */
|
|
1889
1937
|
// Internal row drag — nothing to do here, the row drop handler
|
|
1890
1938
|
// in GridView/ListView already resolved the move.
|
|
1891
1939
|
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
@@ -1959,13 +2007,14 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
|
|
|
1959
2007
|
.filter((n) => n.basename !== '.trash')
|
|
1960
2008
|
.map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
|
|
1961
2009
|
ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
|
|
2010
|
+
ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
|
|
1962
2011
|
ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
|
|
1963
2012
|
ev.dataTransfer.effectAllowed = 'move';
|
|
1964
2013
|
}
|
|
1965
2014
|
|
|
1966
|
-
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string): Promise<void> {
|
|
2015
|
+
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
|
|
1967
2016
|
try {
|
|
1968
|
-
const originWire = qualify(currentPath.value);
|
|
2017
|
+
const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — split panelinden gelen sürüklemede gerçek kaynak klasör */
|
|
1969
2018
|
if (api.endpoints.moveAsync) {
|
|
1970
2019
|
const { op } = await api.moveAsync(sources, targetDir, originWire);
|
|
1971
2020
|
registerMoveUndo(op.id, sources, targetDir, originWire);
|
|
@@ -2006,7 +2055,7 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
|
2006
2055
|
flashToast('Aynı klasöre taşınamaz');
|
|
2007
2056
|
return;
|
|
2008
2057
|
}
|
|
2009
|
-
await
|
|
2058
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2010
2059
|
}
|
|
2011
2060
|
|
|
2012
2061
|
async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
@@ -2025,7 +2074,7 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
|
2025
2074
|
.map((i) => i.path)
|
|
2026
2075
|
.filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
|
|
2027
2076
|
if (sources.length === 0) return;
|
|
2028
|
-
await
|
|
2077
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2029
2078
|
}
|
|
2030
2079
|
|
|
2031
2080
|
function onCancelUpload(job: UploadJob) {
|
|
@@ -2245,6 +2294,244 @@ onBeforeUnmount(() => {
|
|
|
2245
2294
|
rootEl.value?.removeEventListener('fe:tour-restart', onTourRestartEvent);
|
|
2246
2295
|
});
|
|
2247
2296
|
/* === /wiring:c4 === */
|
|
2297
|
+
|
|
2298
|
+
/* === wiring:d1 — sekmeler (tab şeridi) + tab başına split ===
|
|
2299
|
+
*
|
|
2300
|
+
* useTabs, mevcut konum state'inin (currentPath/viewMode) ÜSTÜNDE bir
|
|
2301
|
+
* katmandır: aktif tab gezinmeleri watch ile dinleyip snapshot'ını
|
|
2302
|
+
* günceller; tab geçişi mevcut load(path) yolunu çağırır — yeni fetch
|
|
2303
|
+
* mantığı yok. Şerit tek sekmede hiç render edilmez (embed pixel-aynı).
|
|
2304
|
+
*
|
|
2305
|
+
* Persist: `filex.tabs` — pathPersist scope mantığı izlenir: mode 'none'
|
|
2306
|
+
* ise persist kapalı; rootPath confine'ı anahtara eklenir ki farklı
|
|
2307
|
+
* confine'lı embed'ler birbirinin sekmelerini ezmesin.
|
|
2308
|
+
*/
|
|
2309
|
+
const FE_DND_SRC_MIME = 'application/x-brf-files-src';
|
|
2310
|
+
|
|
2311
|
+
const TABS_LS_BASE = 'filex.tabs';
|
|
2312
|
+
function tabsStorageKey(): string | null {
|
|
2313
|
+
if (persistMode() === 'none') return null;
|
|
2314
|
+
return rootPathProp ? `${TABS_LS_BASE}:${rootPathProp}` : TABS_LS_BASE;
|
|
2315
|
+
}
|
|
2316
|
+
const tabsApi = useTabs({ storageKey: tabsStorageKey() });
|
|
2317
|
+
const tabsRestored = tabsApi.restore();
|
|
2318
|
+
const tabsActiveId = tabsApi.activeId;
|
|
2319
|
+
|
|
2320
|
+
const tabsVisible = computed(() => tabsApi.hasMultiple.value);
|
|
2321
|
+
const activeSplit = computed(() => tabsApi.activeTab.value?.split ?? null);
|
|
2322
|
+
|
|
2323
|
+
// Sekme adı OTOMATİK = güncel klasör adı (kök = depo adı / kök etiketi).
|
|
2324
|
+
function tabLabel(path: string): string {
|
|
2325
|
+
const p = (path || '').replace(/^\/+|\/+$/g, '');
|
|
2326
|
+
if (p === '.trash') return t('node.trash');
|
|
2327
|
+
if (!p) return multiStorageRoot.value ? t('breadcrumb.root') : adapter.value || t('breadcrumb.root');
|
|
2328
|
+
return p.split('/').pop() || p;
|
|
2329
|
+
}
|
|
2330
|
+
const tabItems = computed(() =>
|
|
2331
|
+
tabsApi.tabs.value.map((tb) => ({ id: tb.id, label: tabLabel(tb.path), split: !!tb.split })),
|
|
2332
|
+
);
|
|
2333
|
+
|
|
2334
|
+
// Aktif tab kullanıcıyı izler: gezinme + görünüm değişimi snapshot'a yazılır.
|
|
2335
|
+
watch(currentPath, (p) => tabsApi.syncActive({ path: p }));
|
|
2336
|
+
watch(viewMode, (v) => tabsApi.syncActive({ viewMode: v }));
|
|
2337
|
+
|
|
2338
|
+
// İlk sekme, ilk konum belli olur olmaz tohumlanır (restore varsa dokunma —
|
|
2339
|
+
// aktif snapshot ilk load sonrası currentPath watcher'ıyla zaten senkronlanır).
|
|
2340
|
+
onMounted(() => {
|
|
2341
|
+
if (!tabsRestored) tabsApi.seed(currentPath.value ?? '', viewMode.value);
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
function applyTabLocation(tb: TabState) {
|
|
2345
|
+
if (tb.viewMode && tb.viewMode !== viewMode.value) viewMode.value = tb.viewMode;
|
|
2346
|
+
if (tb.path === '.trash') {
|
|
2347
|
+
void loadTrash();
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
void load(tb.path);
|
|
2351
|
+
}
|
|
2352
|
+
function activateTab(id: string) {
|
|
2353
|
+
const tb = tabsApi.activate(id);
|
|
2354
|
+
if (tb) applyTabLocation(tb);
|
|
2355
|
+
}
|
|
2356
|
+
function newTabHere() {
|
|
2357
|
+
// Mevcut konumu klonlar; görünüm zaten oradadır, load gerekmez.
|
|
2358
|
+
tabsApi.openTab(currentPath.value ?? '', { viewMode: viewMode.value, background: false });
|
|
2359
|
+
}
|
|
2360
|
+
function closeTabById(id: string) {
|
|
2361
|
+
const next = tabsApi.closeTab(id);
|
|
2362
|
+
if (next) applyTabLocation(next);
|
|
2363
|
+
}
|
|
2364
|
+
function nextTab() {
|
|
2365
|
+
const tb = tabsApi.step(1);
|
|
2366
|
+
if (tb) applyTabLocation(tb);
|
|
2367
|
+
}
|
|
2368
|
+
function prevTab() {
|
|
2369
|
+
const tb = tabsApi.step(-1);
|
|
2370
|
+
if (tb) applyTabLocation(tb);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/** Bir klasörü ARKA PLANDA yeni sekmede aç (orta-tık / sağ-tık / palet). */
|
|
2374
|
+
function openNodeInTab(n: FileNode) {
|
|
2375
|
+
if (n.type !== 'dir' || n.basename === '.trash') return;
|
|
2376
|
+
const target = multiStorageRoot.value ? wireToVirtual(n.path) : stripAdapter(n.path);
|
|
2377
|
+
tabsApi.openTab(target, { viewMode: viewMode.value, background: true });
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// Orta-tık delegasyonu: ListView/GridView satırları data-fe-path taşır; kendi
|
|
2381
|
+
// keydown/emit zinciri eklemek yerine kökte tek auxclick dinleyicisi yeter.
|
|
2382
|
+
// (SecondaryPane kendi satırlarında stopPropagation ile kendisi halleder.)
|
|
2383
|
+
function onListAuxClick(ev: MouseEvent) {
|
|
2384
|
+
if (ev.button !== 1) return;
|
|
2385
|
+
const host = ev.target as HTMLElement | null;
|
|
2386
|
+
const el = host && typeof host.closest === 'function' ? host.closest('[data-fe-path]') : null;
|
|
2387
|
+
const p = el?.getAttribute('data-fe-path');
|
|
2388
|
+
if (!p) return;
|
|
2389
|
+
const node = files.value.find((f) => f.path === p);
|
|
2390
|
+
if (!node || node.type !== 'dir' || node.basename === '.trash') return;
|
|
2391
|
+
ev.preventDefault();
|
|
2392
|
+
openNodeInTab(node);
|
|
2393
|
+
}
|
|
2394
|
+
// Orta-tuş mousedown'ı satırlar üzerinde iptal: scroll'lu gövdede Chromium'un
|
|
2395
|
+
// autoscroll'u devreye girer ve auxclick HİÇ üretilmez (canlı teşhis) —
|
|
2396
|
+
// preventDefault autoscroll'u bastırır, auxclick yeniden akar.
|
|
2397
|
+
function onListMiddleDown(ev: MouseEvent) {
|
|
2398
|
+
if (ev.button !== 1) return;
|
|
2399
|
+
const host = ev.target as HTMLElement | null;
|
|
2400
|
+
if (host && typeof host.closest === 'function' && host.closest('[data-fe-path]')) {
|
|
2401
|
+
ev.preventDefault();
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
onMounted(() => {
|
|
2405
|
+
rootEl.value?.addEventListener('auxclick', onListAuxClick);
|
|
2406
|
+
rootEl.value?.addEventListener('mousedown', onListMiddleDown);
|
|
2407
|
+
});
|
|
2408
|
+
onBeforeUnmount(() => {
|
|
2409
|
+
rootEl.value?.removeEventListener('auxclick', onListAuxClick);
|
|
2410
|
+
rootEl.value?.removeEventListener('mousedown', onListMiddleDown);
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// ---- split (tab başına ikincil panel) ------------------------------
|
|
2414
|
+
|
|
2415
|
+
const splitPaneRef = ref<InstanceType<typeof SecondaryPane> | null>(null);
|
|
2416
|
+
// Dar modda split devre dışı (state korunur, genişleyince geri gelir).
|
|
2417
|
+
const splitVisible = computed(() => !!activeSplit.value && !isNarrow.value);
|
|
2418
|
+
|
|
2419
|
+
function toggleSplit() {
|
|
2420
|
+
if (activeSplit.value) {
|
|
2421
|
+
tabsApi.setSplit(null);
|
|
2422
|
+
activePane.value = 'main';
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
if (isNarrow.value) return;
|
|
2426
|
+
tabsApi.setSplit({ path: currentPath.value ?? '' });
|
|
2427
|
+
}
|
|
2428
|
+
function closeSplit() {
|
|
2429
|
+
tabsApi.setSplit(null);
|
|
2430
|
+
activePane.value = 'main';
|
|
2431
|
+
}
|
|
2432
|
+
function onPaneNavigate(p: string) {
|
|
2433
|
+
tabsApi.setSplit({ path: p });
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
|
|
2437
|
+
const activePane = ref<'main' | 'split'>('main');
|
|
2438
|
+
function setPaneMain() {
|
|
2439
|
+
activePane.value = 'main';
|
|
2440
|
+
}
|
|
2441
|
+
watch(splitVisible, (v) => {
|
|
2442
|
+
if (!v) activePane.value = 'main';
|
|
2443
|
+
});
|
|
2444
|
+
const paneIsActive = computed(() => activePane.value === 'split' && splitVisible.value);
|
|
2445
|
+
const mainPaneFocus = computed(() => splitVisible.value && activePane.value === 'main');
|
|
2446
|
+
|
|
2447
|
+
// Pane yardımcıları — hep ana panelin mevcut dönüştürücülerini sarar.
|
|
2448
|
+
function paneToUser(wire: string): string {
|
|
2449
|
+
return multiStorageRoot.value ? wireToVirtual(wire) : stripAdapter(wire);
|
|
2450
|
+
}
|
|
2451
|
+
function paneClamp(p: string): string {
|
|
2452
|
+
const clean = String(p ?? '').replace(/^\/+|\/+$/g, '');
|
|
2453
|
+
if (!rootFloor) return clean;
|
|
2454
|
+
if (!clean || !(clean === rootFloor || clean.startsWith(rootFloor + '/'))) return rootFloor;
|
|
2455
|
+
return clean;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// Pano (clipboard) aktif panele göre: kes/kopyala pane seçiminden beslenir,
|
|
2459
|
+
// yapıştır pane klasörüne iner. State ana panelinkiyle ORTAK — panolar arası
|
|
2460
|
+
// kes-yapıştır bedavaya çalışır.
|
|
2461
|
+
function paneCut() {
|
|
2462
|
+
const nodes = splitPaneRef.value?.selectedNodes() ?? [];
|
|
2463
|
+
if (nodes.length === 0) return;
|
|
2464
|
+
clipboard.value = { mode: 'cut', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
|
|
2465
|
+
flashToast('Kesildi');
|
|
2466
|
+
}
|
|
2467
|
+
function paneCopy() {
|
|
2468
|
+
const nodes = splitPaneRef.value?.selectedNodes() ?? [];
|
|
2469
|
+
if (nodes.length === 0) return;
|
|
2470
|
+
clipboard.value = { mode: 'copy', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
|
|
2471
|
+
flashToast('Kopyalandı');
|
|
2472
|
+
}
|
|
2473
|
+
async function panePaste() {
|
|
2474
|
+
const cb = clipboard.value;
|
|
2475
|
+
const pane = splitPaneRef.value;
|
|
2476
|
+
if (!cb.mode || cb.items.length === 0 || !pane) return;
|
|
2477
|
+
const targetWire = qualify(pane.getPath() ?? '');
|
|
2478
|
+
const originWire = qualify(cb.sourcePath || '') || undefined;
|
|
2479
|
+
if (cb.mode === 'cut' && originWire === targetWire) {
|
|
2480
|
+
flashToast('Aynı klasöre kesilemez');
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy');
|
|
2484
|
+
clipboard.value = { mode: null, items: [], sourcePath: null };
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// ---- paneller arası aktarım ----------------------------------------
|
|
2488
|
+
|
|
2489
|
+
function wireAdapterOf(p: string): string {
|
|
2490
|
+
const i = p.indexOf('://');
|
|
2491
|
+
return i === -1 ? '' : p.slice(0, i);
|
|
2492
|
+
}
|
|
2493
|
+
function dndOrigin(ev: DragEvent): string | undefined {
|
|
2494
|
+
const v = ev.dataTransfer?.getData(FE_DND_SRC_MIME);
|
|
2495
|
+
return v || undefined;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* transferItems — panel-arası / pano aktarımının tek kapısı.
|
|
2500
|
+
* Aynı depo → TAŞI (mevcut moveSourcesAsync yolu: kuyruk + geri al).
|
|
2501
|
+
* Farklı depo → KOPYALA dene; backend cross-storage desteklemiyorsa
|
|
2502
|
+
* i18n'li hata toast'ı. Bitince ikincil panel de tazelenir (ana panel
|
|
2503
|
+
* moveSourcesAsync / pendingOps onSettled üzerinden zaten tazelenir).
|
|
2504
|
+
*/
|
|
2505
|
+
async function transferItems(
|
|
2506
|
+
sources: string[],
|
|
2507
|
+
targetWire: string,
|
|
2508
|
+
originWire?: string,
|
|
2509
|
+
forceCopy = false,
|
|
2510
|
+
): Promise<void> {
|
|
2511
|
+
const list = sources.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
|
|
2512
|
+
if (list.length === 0 || !targetWire) return;
|
|
2513
|
+
const targetAdapter = wireAdapterOf(targetWire);
|
|
2514
|
+
const cross = list.some((p) => wireAdapterOf(p) !== targetAdapter);
|
|
2515
|
+
if (cross || forceCopy) {
|
|
2516
|
+
try {
|
|
2517
|
+
const { op } = await api.copy(list, targetWire);
|
|
2518
|
+
pendingOps.register(op);
|
|
2519
|
+
flashToast(cross ? t('split.cross_copy') : t('split.copy_queued'));
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
|
|
2522
|
+
flashToast(cross ? t('split.cross_failed') : (err as Error).message);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
} else {
|
|
2526
|
+
await moveSourcesAsync(list, targetWire, 'move-transfer', originWire);
|
|
2527
|
+
}
|
|
2528
|
+
void splitPaneRef.value?.reload();
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?: string }) {
|
|
2532
|
+
void transferItems(p.sources, p.targetWire, p.originWire);
|
|
2533
|
+
}
|
|
2534
|
+
/* === /wiring:d1 === */
|
|
2248
2535
|
</script>
|
|
2249
2536
|
|
|
2250
2537
|
<template>
|
|
@@ -2265,6 +2552,21 @@ onBeforeUnmount(() => {
|
|
|
2265
2552
|
@drop="onDropUpload"
|
|
2266
2553
|
@contextmenu="onContextCanvas"
|
|
2267
2554
|
>
|
|
2555
|
+
<!-- wiring:d1 — sekme şeridi: TEK sekmede hiç render edilmez (embed pixel-aynı) -->
|
|
2556
|
+
<TabBar
|
|
2557
|
+
v-if="tabsVisible"
|
|
2558
|
+
:tabs="tabItems"
|
|
2559
|
+
:active-id="tabsActiveId"
|
|
2560
|
+
:locale="locale"
|
|
2561
|
+
:split-enabled="!isNarrow"
|
|
2562
|
+
:split-active="!!activeSplit"
|
|
2563
|
+
@select="activateTab"
|
|
2564
|
+
@close="closeTabById"
|
|
2565
|
+
@new="newTabHere"
|
|
2566
|
+
@reorder="(from: number, to: number) => tabsApi.move(from, to)"
|
|
2567
|
+
@toggle-split="toggleSplit"
|
|
2568
|
+
/>
|
|
2569
|
+
<!-- /wiring:d1 -->
|
|
2268
2570
|
<Toolbar
|
|
2269
2571
|
ref="toolbarRef"
|
|
2270
2572
|
:view-mode="viewMode"
|
|
@@ -2328,14 +2630,19 @@ onBeforeUnmount(() => {
|
|
|
2328
2630
|
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2329
2631
|
as flex siblings (row). Without the inspector open it is visually
|
|
2330
2632
|
identical to the previous direct-child fe__body. -->
|
|
2331
|
-
<div class="fe__main">
|
|
2332
|
-
<div
|
|
2633
|
+
<div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
|
|
2634
|
+
<div
|
|
2635
|
+
class="fe__body"
|
|
2636
|
+
:class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
|
|
2637
|
+
@pointerdown.capture="setPaneMain() /* wiring:d1 */"
|
|
2638
|
+
@click.self="selection.clear()"
|
|
2639
|
+
>
|
|
2333
2640
|
<!-- Initial load: skeleton ghosts (view-mode aware) instead of an
|
|
2334
2641
|
empty/"no files" flash. Only when there's nothing yet — navigation
|
|
2335
2642
|
keeps the current list, exactly as before. -->
|
|
2336
2643
|
<div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
|
|
2337
2644
|
<span class="fe-sr-only">{{ t('loading') }}</span>
|
|
2338
|
-
<div v-if="viewMode
|
|
2645
|
+
<div v-if="viewMode !== 'list' /* wiring:d2 — galeri de grid iskeletini kullanır */" class="fe-skel-grid" aria-hidden="true">
|
|
2339
2646
|
<div v-for="i in 8" :key="i" class="fe-skel-card">
|
|
2340
2647
|
<div class="fe-skel fe-skel--thumb"></div>
|
|
2341
2648
|
<div class="fe-skel fe-skel--label"></div>
|
|
@@ -2504,6 +2811,22 @@ onBeforeUnmount(() => {
|
|
|
2504
2811
|
@star-change="onStarChange"
|
|
2505
2812
|
/>
|
|
2506
2813
|
<GridView
|
|
2814
|
+
v-else-if="viewMode === 'grid' /* wiring:d2 — v-else → v-else-if (3. mod eklendi) */"
|
|
2815
|
+
:files="files"
|
|
2816
|
+
:selected="selection.selected.value"
|
|
2817
|
+
:clipped="clippedPaths"
|
|
2818
|
+
:show-parent-path="!!searchQuery"
|
|
2819
|
+
:locale="locale"
|
|
2820
|
+
:loading="loading"
|
|
2821
|
+
:thumb-src="thumbs.src"
|
|
2822
|
+
@click-card="(n, m) => selection.click(n.path, m)"
|
|
2823
|
+
@dbl-card="openNode"
|
|
2824
|
+
@context-card="onContextTarget"
|
|
2825
|
+
@item-drag-start="onItemDragStart"
|
|
2826
|
+
@item-drop-into="onItemDropInto"
|
|
2827
|
+
/>
|
|
2828
|
+
<!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
|
|
2829
|
+
<GalleryView
|
|
2507
2830
|
v-else
|
|
2508
2831
|
:files="files"
|
|
2509
2832
|
:selected="selection.selected.value"
|
|
@@ -2518,8 +2841,35 @@ onBeforeUnmount(() => {
|
|
|
2518
2841
|
@item-drag-start="onItemDragStart"
|
|
2519
2842
|
@item-drop-into="onItemDropInto"
|
|
2520
2843
|
/>
|
|
2844
|
+
<!-- /wiring:d2 -->
|
|
2521
2845
|
</div>
|
|
2522
2846
|
|
|
2847
|
+
<!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
|
|
2848
|
+
:key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
|
|
2849
|
+
remount olur. -->
|
|
2850
|
+
<SecondaryPane
|
|
2851
|
+
v-if="splitVisible && activeSplit"
|
|
2852
|
+
ref="splitPaneRef"
|
|
2853
|
+
:key="'split-' + tabsActiveId"
|
|
2854
|
+
:api="api"
|
|
2855
|
+
:initial-path="activeSplit.path"
|
|
2856
|
+
:locale="locale"
|
|
2857
|
+
:qualify="qualify"
|
|
2858
|
+
:to-user="paneToUser"
|
|
2859
|
+
:clamp="paneClamp"
|
|
2860
|
+
:root-label="multiStorageRoot ? '/' : adapter || t('breadcrumb.root')"
|
|
2861
|
+
:floor="rootFloor"
|
|
2862
|
+
:multi-root="multiStorageRoot"
|
|
2863
|
+
:virtual-rows="virtualStorageRows"
|
|
2864
|
+
:active="paneIsActive"
|
|
2865
|
+
@navigate="onPaneNavigate"
|
|
2866
|
+
@activate="activePane = 'split'"
|
|
2867
|
+
@close="closeSplit"
|
|
2868
|
+
@open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
|
|
2869
|
+
@transfer="onPaneTransfer"
|
|
2870
|
+
/>
|
|
2871
|
+
<!-- /wiring:d1 -->
|
|
2872
|
+
|
|
2523
2873
|
<!-- koru:k1 — inspector (details) panel; v-if keeps the closed state
|
|
2524
2874
|
free of any DOM. Narrow mode renders it as a full-size overlay. -->
|
|
2525
2875
|
<InspectorPanel
|
|
@@ -2746,13 +3096,16 @@ onBeforeUnmount(() => {
|
|
|
2746
3096
|
@navigate="(p: string) => load(p)"
|
|
2747
3097
|
@new-folder="showNewFolder = true"
|
|
2748
3098
|
@upload="triggerUpload"
|
|
2749
|
-
@toggle-view="viewMode = viewMode === 'list' ? 'grid' : 'list'"
|
|
3099
|
+
@toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
|
|
2750
3100
|
@open-trash="loadTrash"
|
|
2751
3101
|
@refresh="() => load()"
|
|
2752
3102
|
@go-up="goUp"
|
|
2753
3103
|
@open-theme="showThemeGallery = true /* wiring:int */"
|
|
2754
3104
|
@open-shortcut-settings="showShortcutSettings = true /* wiring:int */"
|
|
2755
3105
|
@start-tour="startTour() /* wiring:int */"
|
|
3106
|
+
:split-enabled="!isNarrow /* wiring:d1 */"
|
|
3107
|
+
@tab-new="newTabHere() /* wiring:d1 */"
|
|
3108
|
+
@split-toggle="toggleSplit() /* wiring:d1 */"
|
|
2756
3109
|
/>
|
|
2757
3110
|
<ShortcutsHelp
|
|
2758
3111
|
:open="showShortcutsHelp"
|
|
@@ -39,6 +39,8 @@ const props = defineProps<{
|
|
|
39
39
|
canGoUp?: boolean;
|
|
40
40
|
/* bul:s3 — global search callback; absent = older host, group hidden. */
|
|
41
41
|
globalSearch?: (q: string) => Promise<GlobalSearchHit[]>;
|
|
42
|
+
/* wiring:d1 — false hides the split command (narrow embeds). */
|
|
43
|
+
splitEnabled?: boolean;
|
|
42
44
|
}>();
|
|
43
45
|
|
|
44
46
|
const emit = defineEmits<{
|
|
@@ -57,6 +59,9 @@ const emit = defineEmits<{
|
|
|
57
59
|
(e: 'open-theme'): void;
|
|
58
60
|
(e: 'open-shortcut-settings'): void;
|
|
59
61
|
(e: 'start-tour'): void;
|
|
62
|
+
/* wiring:d1 — tab / split commands */
|
|
63
|
+
(e: 'tab-new'): void;
|
|
64
|
+
(e: 'split-toggle'): void;
|
|
60
65
|
}>();
|
|
61
66
|
|
|
62
67
|
const { t, nodeDisplayName } = useLocale(() => props.locale);
|
|
@@ -126,7 +131,7 @@ const commandItems = computed<PaletteItem[]>(() => {
|
|
|
126
131
|
const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
|
|
127
132
|
{ command: 'new-folder', label: t('toolbar.new_folder'), icon: '📁', enabled: props.canWrite !== false },
|
|
128
133
|
{ command: 'upload', label: t('toolbar.upload'), icon: '⬆', enabled: props.canWrite !== false },
|
|
129
|
-
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : '☰', enabled: true },
|
|
134
|
+
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : props.viewMode === 'grid' ? '▣' : '☰', enabled: true /* wiring:d2 — sıradaki mod ikonu (list→grid→galeri→list) */ },
|
|
130
135
|
{ command: 'open-trash', label: t('cmd.trash'), icon: '🗑', enabled: true },
|
|
131
136
|
{ command: 'refresh', label: t('toolbar.refresh'), icon: '⟳', enabled: true },
|
|
132
137
|
{ command: 'go-up', label: t('toolbar.go_up'), icon: '↑', enabled: props.canGoUp !== false },
|
|
@@ -134,6 +139,9 @@ const commandItems = computed<PaletteItem[]>(() => {
|
|
|
134
139
|
{ command: 'open-theme', label: t('theme.menu'), icon: '🎨', enabled: true },
|
|
135
140
|
{ command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨', enabled: true },
|
|
136
141
|
{ command: 'start-tour', label: t('tour.restart'), icon: '🎓', enabled: true },
|
|
142
|
+
/* wiring:d1 — sekme + split */
|
|
143
|
+
{ command: 'tab-new', label: t('cmd.tab_new'), icon: '⧉', enabled: true },
|
|
144
|
+
{ command: 'split-toggle', label: t('cmd.split_toggle'), icon: '◫', enabled: props.splitEnabled !== false },
|
|
137
145
|
];
|
|
138
146
|
return defs
|
|
139
147
|
.filter((d) => d.enabled && matchScore(d.label, q) >= 0)
|
|
@@ -322,6 +330,9 @@ function choose(item?: PaletteItem) {
|
|
|
322
330
|
case 'open-theme': emit('open-theme'); break;
|
|
323
331
|
case 'open-shortcut-settings': emit('open-shortcut-settings'); break;
|
|
324
332
|
case 'start-tour': emit('start-tour'); break;
|
|
333
|
+
/* wiring:d1 */
|
|
334
|
+
case 'tab-new': emit('tab-new'); break;
|
|
335
|
+
case 'split-toggle': emit('split-toggle'); break;
|
|
325
336
|
}
|
|
326
337
|
}
|
|
327
338
|
}
|