@brftech/filex-core 0.4.2 → 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-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
- package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
- package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
- package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
- package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
- package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
- package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
- package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
- package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
- package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
- package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
- package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
- package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
- package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
- package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
- package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
- package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
- package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
- package/dist/filex-core.js +50 -25
- 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 +597 -4
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +636 -15
- package/src/components/CommandPalette.vue +24 -1
- package/src/components/ContextMenu.vue +114 -8
- package/src/components/GalleryView.vue +214 -0
- package/src/components/GridView.vue +34 -1
- package/src/components/InspectorPanel.vue +170 -0
- package/src/components/ListView.vue +40 -6
- package/src/components/OnboardingTour.vue +332 -0
- package/src/components/OperationsCenter.vue +284 -0
- package/src/components/PendingOpsTray.vue +50 -66
- package/src/components/QuickLook.vue +142 -0
- package/src/components/SecondaryPane.vue +411 -0
- package/src/components/ShortcutSettings.vue +270 -0
- package/src/components/ShortcutsHelp.vue +30 -9
- package/src/components/TabBar.vue +174 -0
- package/src/components/ThemeGallery.vue +253 -0
- package/src/components/Toolbar.vue +75 -11
- package/src/components/UploadProgress.vue +49 -57
- package/src/composables/useFileApi.ts +53 -0
- package/src/composables/useKeyboardShortcuts.ts +340 -137
- package/src/composables/useOperations.ts +291 -0
- package/src/composables/useTabs.ts +222 -0
- package/src/index.ts +51 -0
- package/src/lib/dragGhost.ts +41 -0
- package/src/lib/themes.ts +505 -0
- package/src/locales/en.ts +125 -0
- package/src/locales/tr.ts +125 -0
- package/src/modals/Modal.vue +41 -5
- package/src/styles/base.css +1210 -0
- package/src/types/FileNode.ts +1 -1
- package/dist/index-BLocdbmc.js +0 -7993
- package/dist/index-BLocdbmc.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';
|
|
@@ -50,6 +51,26 @@ import InspectorPanel from './components/InspectorPanel.vue'; /* koru:k1 */
|
|
|
50
51
|
import CommandPalette from './components/CommandPalette.vue';
|
|
51
52
|
import ShortcutsHelp from './components/ShortcutsHelp.vue';
|
|
52
53
|
/* /cila:c wiring */
|
|
54
|
+
/* wiring:c1 — tema galerisi */
|
|
55
|
+
import ThemeGallery from './components/ThemeGallery.vue';
|
|
56
|
+
import { useThemeState, applyThemeToEl, syncThemeStyle } from './lib/themes';
|
|
57
|
+
/* /wiring:c1 */
|
|
58
|
+
/* wiring:c2 — shortcut settings modal + Space quick-look overlay */
|
|
59
|
+
import ShortcutSettings from './components/ShortcutSettings.vue';
|
|
60
|
+
import QuickLook from './components/QuickLook.vue';
|
|
61
|
+
/* /wiring:c2 */
|
|
62
|
+
/* wiring:c3 — unified operations center */
|
|
63
|
+
import OperationsCenter from './components/OperationsCenter.vue';
|
|
64
|
+
import { useOperations } from './composables/useOperations';
|
|
65
|
+
/* /wiring:c3 */
|
|
66
|
+
/* wiring:c4 */
|
|
67
|
+
import OnboardingTour from './components/OnboardingTour.vue';
|
|
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 */
|
|
53
74
|
|
|
54
75
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
55
76
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -152,6 +173,7 @@ const pendingOps = usePendingOps(props.config, api, {
|
|
|
152
173
|
flashToast(`${verb} (${op.progress_total})`);
|
|
153
174
|
}
|
|
154
175
|
void load();
|
|
176
|
+
void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
|
|
155
177
|
},
|
|
156
178
|
});
|
|
157
179
|
|
|
@@ -191,7 +213,7 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
|
|
|
191
213
|
let value: ViewMode = (() => {
|
|
192
214
|
try {
|
|
193
215
|
const stored = localStorage.getItem(VIEW_MODE_KEY);
|
|
194
|
-
if (stored === 'list' || stored === 'grid') return stored;
|
|
216
|
+
if (stored === 'list' || stored === 'grid' || stored === 'gallery') return stored; /* wiring:d2 */
|
|
195
217
|
} catch {
|
|
196
218
|
/* private mode */
|
|
197
219
|
}
|
|
@@ -1069,8 +1091,9 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1069
1091
|
showRename.value = true;
|
|
1070
1092
|
}
|
|
1071
1093
|
},
|
|
1072
|
-
onSelectAll: () => selection.selectAll()
|
|
1094
|
+
onSelectAll: () => (paneIsActive.value ? splitPaneRef.value?.selectAll() : selection.selectAll()) /* wiring:d1 pane-route */,
|
|
1073
1095
|
onOpen: () => {
|
|
1096
|
+
if (paneIsActive.value) return splitPaneRef.value?.openSelected(); /* wiring:d1 pane-route */
|
|
1074
1097
|
const n = selection.nodes.value[0];
|
|
1075
1098
|
if (n) openNode(n);
|
|
1076
1099
|
},
|
|
@@ -1087,10 +1110,10 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1087
1110
|
if (isNarrow.value) closeInspector();
|
|
1088
1111
|
},
|
|
1089
1112
|
onFocusSearch: () => toolbarRef.value?.focusSearch(),
|
|
1090
|
-
onCut: () => cut()
|
|
1091
|
-
onCopy: () => copyToClipboard()
|
|
1092
|
-
onPaste: () => paste()
|
|
1093
|
-
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 */,
|
|
1094
1117
|
/* cila:c wiring */
|
|
1095
1118
|
onPathJump: () => {
|
|
1096
1119
|
showPalette.value = true;
|
|
@@ -1099,7 +1122,14 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1099
1122
|
showShortcutsHelp.value = true;
|
|
1100
1123
|
},
|
|
1101
1124
|
/* /cila:c wiring */
|
|
1125
|
+
onQuickLook: () => quickLookToggle() /* wiring:c2 */,
|
|
1102
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 */
|
|
1103
1133
|
hasSelection: () => !selection.isEmpty.value,
|
|
1104
1134
|
});
|
|
1105
1135
|
|
|
@@ -1354,6 +1384,7 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1354
1384
|
if (!single) return [];
|
|
1355
1385
|
return [
|
|
1356
1386
|
{ key: 'open', label: t('ctx.open'), icon: '↗' },
|
|
1387
|
+
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
|
|
1357
1388
|
];
|
|
1358
1389
|
}
|
|
1359
1390
|
|
|
@@ -1394,6 +1425,7 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
|
|
|
1394
1425
|
const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
|
|
1395
1426
|
return [
|
|
1396
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ç */,
|
|
1397
1429
|
{ key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
|
|
1398
1430
|
{ key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
|
|
1399
1431
|
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w, disabled: !isFile },
|
|
@@ -1519,6 +1551,11 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1519
1551
|
case 'duplicate':
|
|
1520
1552
|
if (targets[0]) await duplicate(targets[0]);
|
|
1521
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 */
|
|
1522
1559
|
}
|
|
1523
1560
|
}
|
|
1524
1561
|
|
|
@@ -1865,11 +1902,38 @@ function onDragLeave() {
|
|
|
1865
1902
|
if (dragCounter.value === 0) dragOver.value = false;
|
|
1866
1903
|
}
|
|
1867
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 */
|
|
1868
1913
|
if (isExternalFileDrag(ev)) {
|
|
1869
1914
|
ev.preventDefault();
|
|
1870
1915
|
}
|
|
1871
1916
|
}
|
|
1872
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 */
|
|
1873
1937
|
// Internal row drag — nothing to do here, the row drop handler
|
|
1874
1938
|
// in GridView/ListView already resolved the move.
|
|
1875
1939
|
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
@@ -1943,13 +2007,14 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
|
|
|
1943
2007
|
.filter((n) => n.basename !== '.trash')
|
|
1944
2008
|
.map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
|
|
1945
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ı */
|
|
1946
2011
|
ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
|
|
1947
2012
|
ev.dataTransfer.effectAllowed = 'move';
|
|
1948
2013
|
}
|
|
1949
2014
|
|
|
1950
|
-
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> {
|
|
1951
2016
|
try {
|
|
1952
|
-
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 */
|
|
1953
2018
|
if (api.endpoints.moveAsync) {
|
|
1954
2019
|
const { op } = await api.moveAsync(sources, targetDir, originWire);
|
|
1955
2020
|
registerMoveUndo(op.id, sources, targetDir, originWire);
|
|
@@ -1990,7 +2055,7 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
|
1990
2055
|
flashToast('Aynı klasöre taşınamaz');
|
|
1991
2056
|
return;
|
|
1992
2057
|
}
|
|
1993
|
-
await
|
|
2058
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
1994
2059
|
}
|
|
1995
2060
|
|
|
1996
2061
|
async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
@@ -2009,7 +2074,7 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
|
2009
2074
|
.map((i) => i.path)
|
|
2010
2075
|
.filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
|
|
2011
2076
|
if (sources.length === 0) return;
|
|
2012
|
-
await
|
|
2077
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2013
2078
|
}
|
|
2014
2079
|
|
|
2015
2080
|
function onCancelUpload(job: UploadJob) {
|
|
@@ -2052,6 +2117,421 @@ async function onCopyPath(adapterPath: string) {
|
|
|
2052
2117
|
function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
2053
2118
|
return api.authHeadersSync({ ...extra });
|
|
2054
2119
|
}
|
|
2120
|
+
|
|
2121
|
+
/* === wiring:c1 — tema galerisi ===
|
|
2122
|
+
* Selected theme (shared module state, localStorage `filex.theme`) is applied
|
|
2123
|
+
* as inline `--fe-*` variables on the explorer root, resolved to the active
|
|
2124
|
+
* light/dark variant — plus a mirrored injected stylesheet for the surfaces
|
|
2125
|
+
* that re-declare tokens (teleported context menu, modal backdrops). Theme
|
|
2126
|
+
* selection is independent from the light/dark mode: the mode only picks
|
|
2127
|
+
* WHICH variant of the theme paints. */
|
|
2128
|
+
const showThemeGallery = ref(false);
|
|
2129
|
+
const { themeId: activeThemeId, setTheme: setActiveTheme } = useThemeState();
|
|
2130
|
+
// Resolved mode: explicit config wins, otherwise the OS preference — the same
|
|
2131
|
+
// logic variables.css encodes in CSS. The inline root variables beat every
|
|
2132
|
+
// stylesheet rule, so they must track this resolution at runtime.
|
|
2133
|
+
const themeMq =
|
|
2134
|
+
typeof window !== 'undefined' && window.matchMedia
|
|
2135
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
2136
|
+
: undefined;
|
|
2137
|
+
const themeOsDark = ref(!!themeMq?.matches);
|
|
2138
|
+
function onThemeMqChange(e: MediaQueryListEvent) {
|
|
2139
|
+
themeOsDark.value = e.matches;
|
|
2140
|
+
}
|
|
2141
|
+
onMounted(() => themeMq?.addEventListener?.('change', onThemeMqChange));
|
|
2142
|
+
onBeforeUnmount(() => themeMq?.removeEventListener?.('change', onThemeMqChange));
|
|
2143
|
+
const themeResolvedDark = computed(
|
|
2144
|
+
() => props.config.theme === 'dark' || (props.config.theme !== 'light' && themeOsDark.value),
|
|
2145
|
+
);
|
|
2146
|
+
watch(
|
|
2147
|
+
[activeThemeId, themeResolvedDark, rootEl],
|
|
2148
|
+
() => {
|
|
2149
|
+
syncThemeStyle(activeThemeId.value);
|
|
2150
|
+
if (rootEl.value) applyThemeToEl(rootEl.value, activeThemeId.value, themeResolvedDark.value);
|
|
2151
|
+
},
|
|
2152
|
+
{ immediate: true },
|
|
2153
|
+
);
|
|
2154
|
+
/* === /wiring:c1 === */
|
|
2155
|
+
/* === wiring:c2 — shortcut settings modal + Space quick-look ===
|
|
2156
|
+
* quickLookTarget follows the selection while the peek is open: arrow
|
|
2157
|
+
* keys emitted by QuickLook move the selection here, and the watcher
|
|
2158
|
+
* below syncs the previewed file. Space toggles (registry action
|
|
2159
|
+
* `quicklook`); Enter promotes the peek into the normal open flow. */
|
|
2160
|
+
const showShortcutSettings = ref(false);
|
|
2161
|
+
const quickLookOpen = ref(false);
|
|
2162
|
+
const quickLookTarget = ref<FileNode | null>(null);
|
|
2163
|
+
|
|
2164
|
+
function quickLookToggle() {
|
|
2165
|
+
if (quickLookOpen.value) {
|
|
2166
|
+
quickLookOpen.value = false;
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const sel = selection.nodes.value;
|
|
2170
|
+
const n = sel.length === 1 ? sel[0] : null;
|
|
2171
|
+
if (!n || n.type !== 'file' || n.basename === '.trash') return;
|
|
2172
|
+
quickLookTarget.value = n;
|
|
2173
|
+
quickLookOpen.value = true;
|
|
2174
|
+
void markRecent(n);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function quickLookNav(delta: number) {
|
|
2178
|
+
const onlyFiles = files.value.filter((f) => f.type === 'file');
|
|
2179
|
+
if (onlyFiles.length === 0) return;
|
|
2180
|
+
const cur = quickLookTarget.value;
|
|
2181
|
+
let idx = cur ? onlyFiles.findIndex((f) => f.path === cur.path) : -1;
|
|
2182
|
+
idx = idx === -1 ? (delta > 0 ? 0 : onlyFiles.length - 1) : (idx + delta + onlyFiles.length) % onlyFiles.length;
|
|
2183
|
+
const next = onlyFiles[idx];
|
|
2184
|
+
if (!next || next.path === cur?.path) return;
|
|
2185
|
+
selection.click(next.path);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function quickLookOpenFull() {
|
|
2189
|
+
const n = quickLookTarget.value;
|
|
2190
|
+
quickLookOpen.value = false;
|
|
2191
|
+
if (n) openNode(n);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
watch(
|
|
2195
|
+
() => selection.nodes.value,
|
|
2196
|
+
(nodes) => {
|
|
2197
|
+
if (!quickLookOpen.value) return;
|
|
2198
|
+
const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
|
|
2199
|
+
if (n && n.path !== quickLookTarget.value?.path) {
|
|
2200
|
+
quickLookTarget.value = n;
|
|
2201
|
+
void markRecent(n);
|
|
2202
|
+
}
|
|
2203
|
+
},
|
|
2204
|
+
);
|
|
2205
|
+
/* === /wiring:c2 === */
|
|
2206
|
+
/* wiring:c3 — operations center store + failed-upload retry */
|
|
2207
|
+
const opsCenter = useOperations();
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Retry a failed upload from the operations center. The failed row is
|
|
2211
|
+
* already retired by the store; re-run the upload against the job's ORIGINAL
|
|
2212
|
+
* target folder (the user may have navigated away since) via the legacy
|
|
2213
|
+
* single-POST path — works for any storage / size, no chunked precondition.
|
|
2214
|
+
*/
|
|
2215
|
+
function retryUploadJob(job: UploadJob) {
|
|
2216
|
+
uploadJobs.value = uploadJobs.value.filter((j) => j.id !== job.id);
|
|
2217
|
+
const file = job.file;
|
|
2218
|
+
const target = job.path || qualify(currentPath.value);
|
|
2219
|
+
const id = crypto.randomUUID();
|
|
2220
|
+
uploadJobs.value = [
|
|
2221
|
+
...uploadJobs.value,
|
|
2222
|
+
{ id, file, path: target, totalBytes: file.size, uploadedBytes: 0, percent: 0, status: 'uploading', cancel() {} },
|
|
2223
|
+
];
|
|
2224
|
+
const patchRetry = (p: Partial<UploadJob>) => {
|
|
2225
|
+
const idx = uploadJobs.value.findIndex((j) => j.id === id);
|
|
2226
|
+
if (idx === -1) return;
|
|
2227
|
+
const next = [...uploadJobs.value];
|
|
2228
|
+
next[idx] = { ...next[idx], ...p };
|
|
2229
|
+
uploadJobs.value = next;
|
|
2230
|
+
};
|
|
2231
|
+
api
|
|
2232
|
+
.uploadMultipart(target, [file], (percent) => {
|
|
2233
|
+
patchRetry({ percent, uploadedBytes: Math.round((percent / 100) * file.size) });
|
|
2234
|
+
emit('upload-progress', { uploadId: id, percent, done: percent >= 100 });
|
|
2235
|
+
})
|
|
2236
|
+
.then(() => {
|
|
2237
|
+
patchRetry({ percent: 100, uploadedBytes: file.size, status: 'done' });
|
|
2238
|
+
emit('upload-progress', { uploadId: id, percent: 100, done: true });
|
|
2239
|
+
void load();
|
|
2240
|
+
})
|
|
2241
|
+
.catch((err: Error) => {
|
|
2242
|
+
patchRetry({ status: 'error', error: err.message });
|
|
2243
|
+
emit('error', { message: err.message, context: { op: 'upload-retry', file: file.name } });
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
/* /wiring:c3 */
|
|
2247
|
+
/* === wiring:c4 — onboarding coach-mark tour ===
|
|
2248
|
+
* First mount with no `filex.tourDone` flag auto-starts the tour (short
|
|
2249
|
+
* delay so the listing/toolbar are laid out). Closing it — finished OR
|
|
2250
|
+
* skipped — stamps the flag; "Turu tekrar başlat" re-opens it any time.
|
|
2251
|
+
* Restart arrives as a bubbled `fe:tour-restart` CustomEvent from the
|
|
2252
|
+
* Toolbar overflow menu, so no extra prop/emit threading through the
|
|
2253
|
+
* shared component tags is needed. */
|
|
2254
|
+
const TOUR_LS_KEY = 'filex.tourDone';
|
|
2255
|
+
const showTour = ref(false);
|
|
2256
|
+
let tourTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2257
|
+
|
|
2258
|
+
function tourAlreadyDone(): boolean {
|
|
2259
|
+
try {
|
|
2260
|
+
return localStorage.getItem(TOUR_LS_KEY) === '1';
|
|
2261
|
+
} catch {
|
|
2262
|
+
return true; // no storage → never auto-nag
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function startTour() {
|
|
2267
|
+
showTour.value = true;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function onTourClose() {
|
|
2271
|
+
showTour.value = false;
|
|
2272
|
+
try {
|
|
2273
|
+
localStorage.setItem(TOUR_LS_KEY, '1');
|
|
2274
|
+
} catch {
|
|
2275
|
+
/* private mode / quota */
|
|
2276
|
+
}
|
|
2277
|
+
rootEl.value?.focus();
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function onTourRestartEvent() {
|
|
2281
|
+
startTour();
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
onMounted(() => {
|
|
2285
|
+
rootEl.value?.addEventListener('fe:tour-restart', onTourRestartEvent);
|
|
2286
|
+
if (!tourAlreadyDone()) {
|
|
2287
|
+
tourTimer = setTimeout(() => {
|
|
2288
|
+
if (!showTour.value) startTour();
|
|
2289
|
+
}, 900);
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
onBeforeUnmount(() => {
|
|
2293
|
+
if (tourTimer) clearTimeout(tourTimer);
|
|
2294
|
+
rootEl.value?.removeEventListener('fe:tour-restart', onTourRestartEvent);
|
|
2295
|
+
});
|
|
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 === */
|
|
2055
2535
|
</script>
|
|
2056
2536
|
|
|
2057
2537
|
<template>
|
|
@@ -2072,6 +2552,21 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2072
2552
|
@drop="onDropUpload"
|
|
2073
2553
|
@contextmenu="onContextCanvas"
|
|
2074
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 -->
|
|
2075
2570
|
<Toolbar
|
|
2076
2571
|
ref="toolbarRef"
|
|
2077
2572
|
:view-mode="viewMode"
|
|
@@ -2089,9 +2584,11 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2089
2584
|
:theme="config.theme || 'auto' /* bag:b4 */"
|
|
2090
2585
|
:inspector-open="showInspector /* koru:k1 */"
|
|
2091
2586
|
@toggle-inspector="toggleInspector /* koru:k1 */"
|
|
2587
|
+
@open-theme="showThemeGallery = true /* wiring:c1 */"
|
|
2092
2588
|
@update:view-mode="viewMode = $event"
|
|
2093
2589
|
@update:search-query="searchQuery = $event"
|
|
2094
2590
|
@update:density="density = $event"
|
|
2591
|
+
@open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
|
|
2095
2592
|
@new-folder="showNewFolder = true"
|
|
2096
2593
|
@upload="triggerUpload"
|
|
2097
2594
|
@refresh="() => load()"
|
|
@@ -2133,14 +2630,19 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2133
2630
|
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2134
2631
|
as flex siblings (row). Without the inspector open it is visually
|
|
2135
2632
|
identical to the previous direct-child fe__body. -->
|
|
2136
|
-
<div class="fe__main">
|
|
2137
|
-
<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
|
+
>
|
|
2138
2640
|
<!-- Initial load: skeleton ghosts (view-mode aware) instead of an
|
|
2139
2641
|
empty/"no files" flash. Only when there's nothing yet — navigation
|
|
2140
2642
|
keeps the current list, exactly as before. -->
|
|
2141
2643
|
<div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
|
|
2142
2644
|
<span class="fe-sr-only">{{ t('loading') }}</span>
|
|
2143
|
-
<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">
|
|
2144
2646
|
<div v-for="i in 8" :key="i" class="fe-skel-card">
|
|
2145
2647
|
<div class="fe-skel fe-skel--thumb"></div>
|
|
2146
2648
|
<div class="fe-skel fe-skel--label"></div>
|
|
@@ -2204,12 +2706,19 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2204
2706
|
<path d="M24 88h72" stroke-dasharray="3 5" />
|
|
2205
2707
|
</svg>
|
|
2206
2708
|
<p class="fe-state__title">{{ t('error.title') }}</p>
|
|
2207
|
-
|
|
2709
|
+
<!-- wiring:c4 — friendly hint + collapsible technical detail; the raw
|
|
2710
|
+
error message used to sit in the hint slot and read like UI copy. -->
|
|
2711
|
+
<p class="fe-state__hint">{{ t('error.hint') }}</p>
|
|
2208
2712
|
<div class="fe-state__actions">
|
|
2209
2713
|
<button type="button" class="fe-btn fe-btn--primary" @click="retryLoad">
|
|
2210
2714
|
{{ t('error.retry') }}
|
|
2211
2715
|
</button>
|
|
2212
2716
|
</div>
|
|
2717
|
+
<details class="fe-state__details">
|
|
2718
|
+
<summary class="fe-state__details-summary">{{ t('error.details') }}</summary>
|
|
2719
|
+
<pre class="fe-state__details-pre">{{ loadError }}</pre>
|
|
2720
|
+
</details>
|
|
2721
|
+
<!-- /wiring:c4 -->
|
|
2213
2722
|
</div>
|
|
2214
2723
|
<!-- Search with zero hits — its own message, not "folder is empty". -->
|
|
2215
2724
|
<div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
|
|
@@ -2302,6 +2811,22 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2302
2811
|
@star-change="onStarChange"
|
|
2303
2812
|
/>
|
|
2304
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
|
|
2305
2830
|
v-else
|
|
2306
2831
|
:files="files"
|
|
2307
2832
|
:selected="selection.selected.value"
|
|
@@ -2316,8 +2841,35 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2316
2841
|
@item-drag-start="onItemDragStart"
|
|
2317
2842
|
@item-drop-into="onItemDropInto"
|
|
2318
2843
|
/>
|
|
2844
|
+
<!-- /wiring:d2 -->
|
|
2319
2845
|
</div>
|
|
2320
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
|
+
|
|
2321
2873
|
<!-- koru:k1 — inspector (details) panel; v-if keeps the closed state
|
|
2322
2874
|
free of any DOM. Narrow mode renders it as a full-size overlay. -->
|
|
2323
2875
|
<InspectorPanel
|
|
@@ -2345,19 +2897,33 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2345
2897
|
</div>
|
|
2346
2898
|
</div>
|
|
2347
2899
|
|
|
2900
|
+
<!-- wiring:c3 — unified operations center. UploadProgress + PendingOpsTray
|
|
2901
|
+
no longer draw their own corner UIs: they are renderless publishers
|
|
2902
|
+
feeding the opsCenter store; the single visible surface is the
|
|
2903
|
+
OperationsCenter badge + panel below. -->
|
|
2348
2904
|
<UploadProgress
|
|
2349
2905
|
:jobs="uploadJobs"
|
|
2350
2906
|
:locale="locale"
|
|
2907
|
+
:center="opsCenter"
|
|
2351
2908
|
@cancel="onCancelUpload"
|
|
2352
2909
|
@dismiss="onDismissUpload"
|
|
2910
|
+
@retry="retryUploadJob"
|
|
2353
2911
|
/>
|
|
2354
2912
|
|
|
2355
2913
|
<PendingOpsTray
|
|
2356
2914
|
:ops="pendingOps.ops.value"
|
|
2357
2915
|
:locale="locale"
|
|
2916
|
+
:center="opsCenter"
|
|
2358
2917
|
@dismiss="(id) => pendingOps.dismiss(id)"
|
|
2359
2918
|
/>
|
|
2360
2919
|
|
|
2920
|
+
<OperationsCenter
|
|
2921
|
+
:center="opsCenter"
|
|
2922
|
+
:locale="locale"
|
|
2923
|
+
:narrow="isNarrow"
|
|
2924
|
+
/>
|
|
2925
|
+
<!-- /wiring:c3 -->
|
|
2926
|
+
|
|
2361
2927
|
<!-- bag:b4 — narrow-mode upload FAB (hidden in trash / read-only /
|
|
2362
2928
|
virtual root; PendingOpsTray+UploadProgress shift up via CSS). -->
|
|
2363
2929
|
<button
|
|
@@ -2530,18 +3096,37 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2530
3096
|
@navigate="(p: string) => load(p)"
|
|
2531
3097
|
@new-folder="showNewFolder = true"
|
|
2532
3098
|
@upload="triggerUpload"
|
|
2533
|
-
@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ü */"
|
|
2534
3100
|
@open-trash="loadTrash"
|
|
2535
3101
|
@refresh="() => load()"
|
|
2536
3102
|
@go-up="goUp"
|
|
3103
|
+
@open-theme="showThemeGallery = true /* wiring:int */"
|
|
3104
|
+
@open-shortcut-settings="showShortcutSettings = true /* wiring:int */"
|
|
3105
|
+
@start-tour="startTour() /* wiring:int */"
|
|
3106
|
+
:split-enabled="!isNarrow /* wiring:d1 */"
|
|
3107
|
+
@tab-new="newTabHere() /* wiring:d1 */"
|
|
3108
|
+
@split-toggle="toggleSplit() /* wiring:d1 */"
|
|
2537
3109
|
/>
|
|
2538
3110
|
<ShortcutsHelp
|
|
2539
3111
|
:open="showShortcutsHelp"
|
|
2540
3112
|
:locale="locale"
|
|
2541
3113
|
@close="showShortcutsHelp = false"
|
|
3114
|
+
@customize="showShortcutsHelp = false; showShortcutSettings = true /* wiring:c2 */"
|
|
2542
3115
|
/>
|
|
2543
3116
|
<!-- /cila:c wiring -->
|
|
2544
3117
|
|
|
3118
|
+
<!-- wiring:c1 — tema galerisi -->
|
|
3119
|
+
<ThemeGallery
|
|
3120
|
+
:open="showThemeGallery"
|
|
3121
|
+
:locale="locale"
|
|
3122
|
+
:theme="config.theme || 'auto'"
|
|
3123
|
+
:dark="themeResolvedDark"
|
|
3124
|
+
:current="activeThemeId"
|
|
3125
|
+
@close="showThemeGallery = false"
|
|
3126
|
+
@select="setActiveTheme"
|
|
3127
|
+
/>
|
|
3128
|
+
<!-- /wiring:c1 -->
|
|
3129
|
+
|
|
2545
3130
|
<input
|
|
2546
3131
|
ref="fileInputEl"
|
|
2547
3132
|
type="file"
|
|
@@ -2567,6 +3152,42 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
|
|
|
2567
3152
|
>{{ toast.actionLabel }}</button>
|
|
2568
3153
|
</div>
|
|
2569
3154
|
</transition>
|
|
3155
|
+
|
|
3156
|
+
<!-- wiring:c2 — shortcut settings modal + Space quick-look overlay -->
|
|
3157
|
+
<ShortcutSettings
|
|
3158
|
+
:open="showShortcutSettings"
|
|
3159
|
+
:locale="locale"
|
|
3160
|
+
:theme="config.theme || 'auto'"
|
|
3161
|
+
@close="showShortcutSettings = false"
|
|
3162
|
+
/>
|
|
3163
|
+
<QuickLook
|
|
3164
|
+
:open="quickLookOpen"
|
|
3165
|
+
:locale="locale"
|
|
3166
|
+
:file="quickLookTarget"
|
|
3167
|
+
:theme="config.theme || 'auto'"
|
|
3168
|
+
:preview-url="(p: string) => api.previewUrl(p)"
|
|
3169
|
+
:download-url="(p: string) => api.downloadUrl(p)"
|
|
3170
|
+
:only-office-base="effectiveOnlyOfficeBase"
|
|
3171
|
+
:only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
|
|
3172
|
+
:auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
|
|
3173
|
+
:auth-credentials="api.credentialsMode()"
|
|
3174
|
+
:drawio-url="effectiveDrawioUrl"
|
|
3175
|
+
:pdf-worker-url="props.config.pdfWorkerUrl || null"
|
|
3176
|
+
:viewer-base-url="props.config.viewerBaseUrl || null"
|
|
3177
|
+
@close="quickLookOpen = false"
|
|
3178
|
+
@nav="quickLookNav"
|
|
3179
|
+
@open-full="quickLookOpenFull"
|
|
3180
|
+
/>
|
|
3181
|
+
<!-- /wiring:c2 -->
|
|
3182
|
+
<!-- wiring:c4 — onboarding coach-mark tour (teleports itself to body) -->
|
|
3183
|
+
<OnboardingTour
|
|
3184
|
+
:open="showTour"
|
|
3185
|
+
:locale="locale"
|
|
3186
|
+
:root="rootEl"
|
|
3187
|
+
:theme="config.theme || 'auto'"
|
|
3188
|
+
@close="onTourClose"
|
|
3189
|
+
/>
|
|
3190
|
+
<!-- /wiring:c4 -->
|
|
2570
3191
|
</div>
|
|
2571
3192
|
</template>
|
|
2572
3193
|
|