@brftech/filex-core 0.7.0 → 0.7.2
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-w6GDJJt9.js → ArchiveViewer-DAb5peBU.js} +2 -2
- package/dist/{ArchiveViewer-w6GDJJt9.js.map → ArchiveViewer-DAb5peBU.js.map} +1 -1
- package/dist/{CsvViewer-Ci5-mke8.js → CsvViewer-_bnmnpjn.js} +2 -2
- package/dist/{CsvViewer-Ci5-mke8.js.map → CsvViewer-_bnmnpjn.js.map} +1 -1
- package/dist/{DrawioViewer-D2iMuQfD.js → DrawioViewer-Bjns4ob8.js} +2 -2
- package/dist/{DrawioViewer-D2iMuQfD.js.map → DrawioViewer-Bjns4ob8.js.map} +1 -1
- package/dist/{EpubViewer-D4ydsa9X.js → EpubViewer-DoFuZTtA.js} +2 -2
- package/dist/{EpubViewer-D4ydsa9X.js.map → EpubViewer-DoFuZTtA.js.map} +1 -1
- package/dist/{IpynbViewer-CWBwiFKX.js → IpynbViewer-eg1ySMD4.js} +2 -2
- package/dist/{IpynbViewer-CWBwiFKX.js.map → IpynbViewer-eg1ySMD4.js.map} +1 -1
- package/dist/{MermaidViewer-CDhuqQli.js → MermaidViewer-BZ13vA6S.js} +2 -2
- package/dist/{MermaidViewer-CDhuqQli.js.map → MermaidViewer-BZ13vA6S.js.map} +1 -1
- package/dist/{PsdViewer-C_TkFb2H.js → PsdViewer-qFkE4KCB.js} +2 -2
- package/dist/{PsdViewer-C_TkFb2H.js.map → PsdViewer-qFkE4KCB.js.map} +1 -1
- package/dist/{TiffViewer-BmWCXR7e.js → TiffViewer-eHJTCYsd.js} +2 -2
- package/dist/{TiffViewer-BmWCXR7e.js.map → TiffViewer-eHJTCYsd.js.map} +1 -1
- package/dist/{Viewer3D-5kPT1QsY.js → Viewer3D-C4--ljdO.js} +2 -2
- package/dist/{Viewer3D-5kPT1QsY.js.map → Viewer3D-C4--ljdO.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +56 -56
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BSDT0PU8.js +11859 -0
- package/dist/index-BSDT0PU8.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +106 -12
- package/src/components/SecondaryPane.vue +88 -78
- package/src/components/Toolbar.vue +112 -3
- package/src/composables/useKeyboardShortcuts.ts +6 -0
- package/src/composables/useTabs.ts +11 -2
- package/src/styles/base.css +50 -4
- package/src/styles/variables.css +2 -2
- package/dist/index-nc0-oI_l.js +0 -11723
- package/dist/index-nc0-oI_l.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -1341,9 +1341,11 @@ function openNodeInNewTab(n: FileNode) {
|
|
|
1341
1341
|
void markRecent(n);
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
|
-
type ContextMode = 'selection' | 'breadcrumb'
|
|
1344
|
+
type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — yan panel sağ-tık */;
|
|
1345
1345
|
const ctxMode = ref<ContextMode>('selection');
|
|
1346
1346
|
const breadcrumbCtxPath = ref<string>('');
|
|
1347
|
+
/* ui-fix — yan panel sağ-tık menüsünün hedef node'ları (pane selection). */
|
|
1348
|
+
const paneCtxTargets = ref<FileNode[]>([]);
|
|
1347
1349
|
const breadcrumbCtxLabel = ref<string>('');
|
|
1348
1350
|
|
|
1349
1351
|
const selectionMode = computed<SelectionMode>(() => {
|
|
@@ -1388,6 +1390,61 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
|
|
|
1388
1390
|
ctxRef.value?.show({ clientX: payload.x, clientY: payload.y }, []);
|
|
1389
1391
|
}
|
|
1390
1392
|
|
|
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). */
|
|
1397
|
+
function onPaneContext(node: FileNode | null, ev: MouseEvent) {
|
|
1398
|
+
activePane.value = 'split';
|
|
1399
|
+
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.
|
|
1402
|
+
paneCtxTargets.value = node ? (sel.length > 0 ? sel : [node]) : [];
|
|
1403
|
+
ctxMode.value = 'pane';
|
|
1404
|
+
ctxRef.value?.show({ clientX: ev.clientX, clientY: ev.clientY }, paneCtxTargets.value);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
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
|
+
|
|
1391
1448
|
const contextActions = computed<ContextAction[]>(() => {
|
|
1392
1449
|
if (ctxMode.value === 'breadcrumb') {
|
|
1393
1450
|
return [
|
|
@@ -1395,6 +1452,9 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1395
1452
|
{ key: 'copy-path', label: t('breadcrumb.copy_path'), icon: '📋' },
|
|
1396
1453
|
];
|
|
1397
1454
|
}
|
|
1455
|
+
if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü */) {
|
|
1456
|
+
return paneActionList(paneCtxTargets.value);
|
|
1457
|
+
}
|
|
1398
1458
|
|
|
1399
1459
|
const sel = selection.nodes.value;
|
|
1400
1460
|
const any = sel.length > 0;
|
|
@@ -1517,6 +1577,10 @@ async function onContextAction(action: ContextAction, targets: FileNode[]) {
|
|
|
1517
1577
|
}
|
|
1518
1578
|
return;
|
|
1519
1579
|
}
|
|
1580
|
+
if (ctxMode.value === 'pane' /* ui-fix — yan panel aksiyonları pane-route */) {
|
|
1581
|
+
await dispatchPaneAction(action.key, paneCtxTargets.value);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1520
1584
|
await dispatchItemAction(action.key, targets);
|
|
1521
1585
|
}
|
|
1522
1586
|
|
|
@@ -1581,7 +1645,10 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1581
1645
|
flashToast('Kopyala → Yapıştır hazır');
|
|
1582
1646
|
break;
|
|
1583
1647
|
case 'paste':
|
|
1584
|
-
|
|
1648
|
+
/* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
|
|
1649
|
+
* (klavye kısayolu zaten pane-route'luydu; menü değildi). */
|
|
1650
|
+
if (paneIsActive.value) await panePaste();
|
|
1651
|
+
else await paste();
|
|
1585
1652
|
break;
|
|
1586
1653
|
case 'delete':
|
|
1587
1654
|
showDelete.value = true;
|
|
@@ -2518,14 +2585,30 @@ function toggleSplit() {
|
|
|
2518
2585
|
return;
|
|
2519
2586
|
}
|
|
2520
2587
|
if (isNarrow.value) return;
|
|
2521
|
-
tabsApi.setSplit({ path: currentPath.value ?? '' });
|
|
2588
|
+
tabsApi.setSplit({ path: currentPath.value ?? '', viewMode: viewMode.value });
|
|
2522
2589
|
}
|
|
2523
2590
|
function closeSplit() {
|
|
2524
2591
|
tabsApi.setSplit(null);
|
|
2525
2592
|
activePane.value = 'main';
|
|
2526
2593
|
}
|
|
2527
2594
|
function onPaneNavigate(p: string) {
|
|
2528
|
-
tabsApi.setSplit({ path: p });
|
|
2595
|
+
tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
|
|
2596
|
+
}
|
|
2597
|
+
/* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
|
|
2598
|
+
* devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
|
|
2599
|
+
* toggle'ı AKTİF panele yazar (Burak: "B tıklıyken ikon değiştir dersem
|
|
2600
|
+
* B'nin değişmesi lazım"). */
|
|
2601
|
+
const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
|
|
2602
|
+
function setPaneViewMode(v: ViewMode) {
|
|
2603
|
+
if (!activeSplit.value) return;
|
|
2604
|
+
tabsApi.setSplit({ ...activeSplit.value, viewMode: v });
|
|
2605
|
+
}
|
|
2606
|
+
const displayedViewMode = computed<ViewMode>(() =>
|
|
2607
|
+
paneIsActive.value ? paneViewMode.value : viewMode.value,
|
|
2608
|
+
);
|
|
2609
|
+
function setDisplayedViewMode(v: ViewMode) {
|
|
2610
|
+
if (paneIsActive.value) setPaneViewMode(v);
|
|
2611
|
+
else viewMode.value = v;
|
|
2529
2612
|
}
|
|
2530
2613
|
|
|
2531
2614
|
// Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
|
|
@@ -2883,7 +2966,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
2883
2966
|
<!-- /wiring:d1 -->
|
|
2884
2967
|
<Toolbar
|
|
2885
2968
|
ref="toolbarRef"
|
|
2886
|
-
:view-mode="
|
|
2969
|
+
:view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
|
|
2887
2970
|
:search-query="searchQuery"
|
|
2888
2971
|
:trash-active="trashActive"
|
|
2889
2972
|
:actions="toolbarActions"
|
|
@@ -2899,7 +2982,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
2899
2982
|
:inspector-open="showInspector /* koru:k1 */"
|
|
2900
2983
|
@toggle-inspector="toggleInspector /* koru:k1 */"
|
|
2901
2984
|
@open-theme="showThemeGallery = true /* wiring:c1 */"
|
|
2902
|
-
@update:view-mode="
|
|
2985
|
+
@update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
|
|
2903
2986
|
@update:search-query="searchQuery = $event"
|
|
2904
2987
|
@update:density="density = $event"
|
|
2905
2988
|
@open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
|
|
@@ -2911,6 +2994,18 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
2911
2994
|
@open-recents="showRecents = true"
|
|
2912
2995
|
/>
|
|
2913
2996
|
|
|
2997
|
+
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2998
|
+
as flex siblings (row). Without the inspector open it is visually
|
|
2999
|
+
identical to the previous direct-child fe__body. -->
|
|
3000
|
+
<div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
|
|
3001
|
+
<!-- ui-fix — sol panelin başlığı (breadcrumb + durum şeritleri + body)
|
|
3002
|
+
tek bir sarmalda: split modunda bu sarmal sol yarıya sığar, böylece
|
|
3003
|
+
breadcrumb tüm sayfayı değil kendi panelini kaplar (SecondaryPane'in
|
|
3004
|
+
kendi kırıntısıyla simetrik). Aktif-panel vurgusu da bu sarmalda. -->
|
|
3005
|
+
<div
|
|
3006
|
+
class="fe__primary"
|
|
3007
|
+
:class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
|
|
3008
|
+
>
|
|
2914
3009
|
<Breadcrumb
|
|
2915
3010
|
:dirname="dirname"
|
|
2916
3011
|
:adapter="adapter"
|
|
@@ -2952,13 +3047,8 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
2952
3047
|
</div>
|
|
2953
3048
|
<!-- /wiring:e2 -->
|
|
2954
3049
|
|
|
2955
|
-
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2956
|
-
as flex siblings (row). Without the inspector open it is visually
|
|
2957
|
-
identical to the previous direct-child fe__body. -->
|
|
2958
|
-
<div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
|
|
2959
3050
|
<div
|
|
2960
3051
|
class="fe__body"
|
|
2961
|
-
:class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
|
|
2962
3052
|
@pointerdown.capture="setPaneMain() /* wiring:d1 */"
|
|
2963
3053
|
@click.self="selection.clear()"
|
|
2964
3054
|
>
|
|
@@ -3211,6 +3301,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3211
3301
|
/>
|
|
3212
3302
|
<!-- /wiring:d2 -->
|
|
3213
3303
|
</div>
|
|
3304
|
+
</div><!-- /fe__primary ui-fix -->
|
|
3214
3305
|
|
|
3215
3306
|
<!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
|
|
3216
3307
|
:key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
|
|
@@ -3230,11 +3321,14 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3230
3321
|
:multi-root="multiStorageRoot"
|
|
3231
3322
|
:virtual-rows="virtualStorageRows"
|
|
3232
3323
|
:active="paneIsActive"
|
|
3324
|
+
:view-mode="paneViewMode /* ui-fix */"
|
|
3325
|
+
:thumb-src="thumbs.src /* ui-fix */"
|
|
3233
3326
|
@navigate="onPaneNavigate"
|
|
3234
3327
|
@activate="activePane = 'split'"
|
|
3235
3328
|
@close="closeSplit"
|
|
3236
3329
|
@open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
|
|
3237
3330
|
@transfer="onPaneTransfer"
|
|
3331
|
+
@context="onPaneContext /* ui-fix — yan panel sağ-tık menüsü */"
|
|
3238
3332
|
/>
|
|
3239
3333
|
<!-- /wiring:d1 -->
|
|
3240
3334
|
|
|
@@ -3476,7 +3570,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3476
3570
|
@navigate="(p: string) => load(p)"
|
|
3477
3571
|
@new-folder="showNewFolder = true"
|
|
3478
3572
|
@upload="triggerUpload"
|
|
3479
|
-
@toggle-view="
|
|
3573
|
+
@toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
|
|
3480
3574
|
@open-trash="loadTrash"
|
|
3481
3575
|
@refresh="() => load()"
|
|
3482
3576
|
@go-up="goUp"
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { computed, ref } from 'vue';
|
|
20
20
|
import type { FileApi } from '../composables/useFileApi';
|
|
21
|
-
import type { FileNode } from '../types/FileNode';
|
|
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
|
|
24
|
+
import ListView from './ListView.vue';
|
|
25
|
+
import GridView from './GridView.vue';
|
|
26
|
+
import GalleryView from './GalleryView.vue';
|
|
25
27
|
|
|
26
28
|
// Same literals ListView/GridView already hardcode for the internal DnD
|
|
27
29
|
// channel; `-src` carries the origin directory so the host can move (and
|
|
@@ -50,6 +52,11 @@ const props = defineProps<{
|
|
|
50
52
|
virtualRows?: () => FileNode[];
|
|
51
53
|
/** Active-panel highlight (keyboard target). */
|
|
52
54
|
active?: boolean;
|
|
55
|
+
/** ui-fix — the pane renders the SAME view components as the main
|
|
56
|
+
* panel (list/grid/gallery) instead of its own flat list. */
|
|
57
|
+
viewMode?: ViewMode;
|
|
58
|
+
/** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
|
|
59
|
+
thumbSrc?: (n: FileNode) => string | null;
|
|
53
60
|
}>();
|
|
54
61
|
|
|
55
62
|
const emit = defineEmits<{
|
|
@@ -58,9 +65,12 @@ const emit = defineEmits<{
|
|
|
58
65
|
(e: 'close'): void;
|
|
59
66
|
(e: 'open-tab', path: string): void;
|
|
60
67
|
(e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
|
|
68
|
+
/* ui-fix — yan panelde sağ-tık: menü ana bileşende (FileExplorer) açılır.
|
|
69
|
+
* node=null → boş alana sağ-tık (seçimsiz, yalnız Yapıştır). */
|
|
70
|
+
(e: 'context', node: FileNode | null, ev: MouseEvent): void;
|
|
61
71
|
}>();
|
|
62
72
|
|
|
63
|
-
const { t
|
|
73
|
+
const { t } = useLocale(() => props.locale);
|
|
64
74
|
|
|
65
75
|
const path = ref<string>('');
|
|
66
76
|
const files = ref<FileNode[]>([]);
|
|
@@ -149,35 +159,52 @@ function crumbGo(target: string) {
|
|
|
149
159
|
// Selection + navigation
|
|
150
160
|
// ------------------------------------------------------------------
|
|
151
161
|
|
|
152
|
-
|
|
162
|
+
/* ui-fix — adapter for the shared view components' click contract
|
|
163
|
+
* ({ctrl, shift} mod object instead of a MouseEvent). Shift behaves as
|
|
164
|
+
* ctrl here: the pane keeps a simple Set, no range anchor. */
|
|
165
|
+
function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean }) {
|
|
153
166
|
emit('activate');
|
|
154
|
-
const multi =
|
|
167
|
+
const multi = mod.ctrl || mod.shift;
|
|
155
168
|
const next = new Set<string>(multi ? selected.value : []);
|
|
156
169
|
if (multi && next.has(n.path)) next.delete(n.path);
|
|
157
170
|
else next.add(n.path);
|
|
158
171
|
selected.value = next;
|
|
159
172
|
}
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
/* ui-fix — right-click on a pane item: activate + select it, then let the
|
|
175
|
+
* host open the pane context menu (its actions are pane-routed). Selecting
|
|
176
|
+
* first keeps the menu's targets on THIS pane, not the main panel. */
|
|
177
|
+
function onViewContext(n: FileNode, ev: MouseEvent) {
|
|
178
|
+
ev.preventDefault();
|
|
179
|
+
ev.stopPropagation();
|
|
180
|
+
emit('activate');
|
|
181
|
+
if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
|
|
182
|
+
emit('context', n, ev);
|
|
164
183
|
}
|
|
165
184
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
/* ui-fix — boş alana (arka plan) sağ-tık: seçimsiz menü (yalnız Yapıştır)
|
|
186
|
+
* → boş klasöre / mevcut klasöre dosya yapıştırma. Satır menüsü stopProp
|
|
187
|
+
* ettiğinden bu yalnız gerçek boş alanda tetiklenir. */
|
|
188
|
+
function onBgContext(ev: MouseEvent) {
|
|
189
|
+
ev.preventDefault();
|
|
190
|
+
ev.stopPropagation(); // FileExplorer kökündeki canvas menüsüne bubble etmesin
|
|
191
|
+
emit('activate');
|
|
192
|
+
selected.value = new Set();
|
|
193
|
+
emit('context', null, ev);
|
|
170
194
|
}
|
|
171
195
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
ev
|
|
178
|
-
ev.
|
|
196
|
+
/* ui-fix — drop-into from the shared views: same payload path as the
|
|
197
|
+
* old flat rows (dir check + wire-qualified row paths). */
|
|
198
|
+
function onViewDropInto(target: FileNode, ev: DragEvent) {
|
|
199
|
+
dropBg.value = false;
|
|
200
|
+
if (target.type !== 'dir' || isStorageRow(target)) return;
|
|
201
|
+
if (!acceptDrag(ev)) return;
|
|
202
|
+
handleDropPayload(ev, target.path);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function onRowDbl(n: FileNode) {
|
|
179
206
|
if (n.type !== 'dir') return;
|
|
180
|
-
|
|
207
|
+
void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
|
|
181
208
|
}
|
|
182
209
|
|
|
183
210
|
function clearSelection() {
|
|
@@ -212,7 +239,6 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
|
|
|
212
239
|
// Drop target (dir rows + pane background)
|
|
213
240
|
// ------------------------------------------------------------------
|
|
214
241
|
|
|
215
|
-
const dropPath = ref<string | null>(null);
|
|
216
242
|
const dropBg = ref(false);
|
|
217
243
|
|
|
218
244
|
function acceptDrag(ev: DragEvent): boolean {
|
|
@@ -238,28 +264,6 @@ function handleDropPayload(ev: DragEvent, targetWire: string) {
|
|
|
238
264
|
emit('transfer', { sources, targetWire, originWire: origin });
|
|
239
265
|
}
|
|
240
266
|
|
|
241
|
-
function onRowDragOver(n: FileNode, ev: DragEvent) {
|
|
242
|
-
if (n.type !== 'dir' || isStorageRow(n)) return;
|
|
243
|
-
if (!acceptDrag(ev)) return;
|
|
244
|
-
ev.preventDefault();
|
|
245
|
-
ev.stopPropagation();
|
|
246
|
-
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
247
|
-
dropPath.value = n.path;
|
|
248
|
-
dropBg.value = false;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function onRowDragLeave(n: FileNode) {
|
|
252
|
-
if (dropPath.value === n.path) dropPath.value = null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function onRowDrop(n: FileNode, ev: DragEvent) {
|
|
256
|
-
dropPath.value = null;
|
|
257
|
-
dropBg.value = false;
|
|
258
|
-
if (n.type !== 'dir' || isStorageRow(n)) return;
|
|
259
|
-
if (!acceptDrag(ev)) return;
|
|
260
|
-
handleDropPayload(ev, n.path); // real rows are wire-qualified
|
|
261
|
-
}
|
|
262
|
-
|
|
263
267
|
function onBgDragOver(ev: DragEvent) {
|
|
264
268
|
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
265
269
|
ev.preventDefault();
|
|
@@ -274,7 +278,6 @@ function onBgDragLeave() {
|
|
|
274
278
|
|
|
275
279
|
function onBgDrop(ev: DragEvent) {
|
|
276
280
|
dropBg.value = false;
|
|
277
|
-
dropPath.value = null;
|
|
278
281
|
if (!acceptDrag(ev) || atVirtualRoot.value) return;
|
|
279
282
|
handleDropPayload(ev, props.qualify(path.value));
|
|
280
283
|
}
|
|
@@ -313,10 +316,6 @@ function getPath(): string {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
316
|
-
|
|
317
|
-
function specialEmojiFor(n: FileNode): string | null {
|
|
318
|
-
return isStorageRow(n) ? '💾' : null;
|
|
319
|
-
}
|
|
320
319
|
</script>
|
|
321
320
|
|
|
322
321
|
<template>
|
|
@@ -360,6 +359,7 @@ function specialEmojiFor(n: FileNode): string | null {
|
|
|
360
359
|
class="fe-split__body"
|
|
361
360
|
:class="{ 'is-dropover': dropBg }"
|
|
362
361
|
@click.self="clearSelection"
|
|
362
|
+
@contextmenu="onBgContext"
|
|
363
363
|
@dragover="onBgDragOver"
|
|
364
364
|
@dragleave="onBgDragLeave"
|
|
365
365
|
@drop="onBgDrop"
|
|
@@ -376,36 +376,46 @@ function specialEmojiFor(n: FileNode): string | null {
|
|
|
376
376
|
<div v-else-if="files.length === 0" class="fe-split__state">
|
|
377
377
|
{{ t('empty.folder') }}
|
|
378
378
|
</div>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
379
|
+
<!-- ui-fix — aynı görünüm bileşenleri (list/grid/gallery), pane'in
|
|
380
|
+
kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
|
|
381
|
+
<ListView
|
|
382
|
+
v-else-if="(viewMode ?? 'list') === 'list'"
|
|
383
|
+
:files="files"
|
|
384
|
+
:selected="selected"
|
|
385
|
+
:locale="locale"
|
|
386
|
+
:loading="loading"
|
|
387
|
+
@click-row="onViewClick"
|
|
388
|
+
@dbl-row="onRowDbl"
|
|
389
|
+
@context-row="onViewContext"
|
|
390
|
+
@item-drag-start="onRowDragStart"
|
|
391
|
+
@item-drop-into="onViewDropInto"
|
|
392
|
+
/>
|
|
393
|
+
<GridView
|
|
394
|
+
v-else-if="viewMode === 'grid'"
|
|
395
|
+
:files="files"
|
|
396
|
+
:selected="selected"
|
|
397
|
+
:locale="locale"
|
|
398
|
+
:loading="loading"
|
|
399
|
+
:thumb-src="thumbSrc"
|
|
400
|
+
@click-card="onViewClick"
|
|
401
|
+
@dbl-card="onRowDbl"
|
|
402
|
+
@context-card="onViewContext"
|
|
403
|
+
@item-drag-start="onRowDragStart"
|
|
404
|
+
@item-drop-into="onViewDropInto"
|
|
405
|
+
/>
|
|
406
|
+
<GalleryView
|
|
407
|
+
v-else
|
|
408
|
+
:files="files"
|
|
409
|
+
:selected="selected"
|
|
410
|
+
:locale="locale"
|
|
411
|
+
:loading="loading"
|
|
412
|
+
:thumb-src="thumbSrc"
|
|
413
|
+
@click-card="onViewClick"
|
|
414
|
+
@dbl-card="onRowDbl"
|
|
415
|
+
@context-card="onViewContext"
|
|
416
|
+
@item-drag-start="onRowDragStart"
|
|
417
|
+
@item-drop-into="onViewDropInto"
|
|
418
|
+
/>
|
|
409
419
|
</div>
|
|
410
420
|
</section>
|
|
411
421
|
</template>
|
|
@@ -152,6 +152,74 @@ const mode = computed<SelectionMode>(() => props.selectionMode ?? 'none');
|
|
|
152
152
|
// context menu uses, the two menus are guaranteed to match.
|
|
153
153
|
const toolbarItems = computed(() => props.actions.filter((a) => !a.divider && !a.hidden));
|
|
154
154
|
|
|
155
|
+
/* === ui-fix — wide-mode action folding ===============================
|
|
156
|
+
* Long selection-action rows used to wrap the whole toolbar and push the
|
|
157
|
+
* search / view controls onto a second line. The row is now single-line:
|
|
158
|
+
* actions that do not fit fold into a "⋯" menu. Widths come from a
|
|
159
|
+
* hidden measurement strip (all actions always rendered there), so the
|
|
160
|
+
* calculation never mutates what it measures. */
|
|
161
|
+
const primaryEl = ref<HTMLElement | null>(null);
|
|
162
|
+
const measureEl = ref<HTMLElement | null>(null);
|
|
163
|
+
const wideMoreBtnEl = ref<HTMLElement | null>(null);
|
|
164
|
+
const wideMoreRef = ref<InstanceType<typeof ContextMenu> | null>(null);
|
|
165
|
+
const visibleActionCount = ref(Number.MAX_SAFE_INTEGER);
|
|
166
|
+
const visibleToolbarItems = computed(() => toolbarItems.value.slice(0, visibleActionCount.value));
|
|
167
|
+
const overflowToolbarItems = computed(() => toolbarItems.value.slice(visibleActionCount.value));
|
|
168
|
+
const WIDE_MORE_BTN_W = 40;
|
|
169
|
+
|
|
170
|
+
function recalcFold() {
|
|
171
|
+
if (props.narrow) return;
|
|
172
|
+
const cont = primaryEl.value;
|
|
173
|
+
const meas = measureEl.value;
|
|
174
|
+
if (!cont || !meas) {
|
|
175
|
+
visibleActionCount.value = toolbarItems.value.length;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const gap = parseFloat(getComputedStyle(cont).gap) || 8;
|
|
179
|
+
let fixed = 0;
|
|
180
|
+
for (const child of Array.from(cont.children) as HTMLElement[]) {
|
|
181
|
+
if (child === meas || child === wideMoreBtnEl.value) continue;
|
|
182
|
+
if (child.classList.contains('fe-btn--fold')) continue;
|
|
183
|
+
if (child.offsetWidth > 0) fixed += child.offsetWidth + gap;
|
|
184
|
+
}
|
|
185
|
+
const widths = (Array.from(meas.children) as HTMLElement[]).map((c) => c.offsetWidth + gap);
|
|
186
|
+
const total = widths.reduce((s, w) => s + w, 0);
|
|
187
|
+
const avail = cont.clientWidth - fixed;
|
|
188
|
+
if (total <= avail) {
|
|
189
|
+
visibleActionCount.value = widths.length;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let used = WIDE_MORE_BTN_W + gap;
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const w of widths) {
|
|
195
|
+
if (used + w > avail) break;
|
|
196
|
+
used += w;
|
|
197
|
+
count += 1;
|
|
198
|
+
}
|
|
199
|
+
visibleActionCount.value = count;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let foldRo: ResizeObserver | undefined;
|
|
203
|
+
onMounted(() => {
|
|
204
|
+
if (typeof ResizeObserver !== 'undefined' && primaryEl.value) {
|
|
205
|
+
foldRo = new ResizeObserver(() => recalcFold());
|
|
206
|
+
foldRo.observe(primaryEl.value);
|
|
207
|
+
}
|
|
208
|
+
void nextTick(recalcFold);
|
|
209
|
+
});
|
|
210
|
+
onBeforeUnmount(() => foldRo?.disconnect());
|
|
211
|
+
watch(
|
|
212
|
+
() => [toolbarItems.value, props.narrow, props.trashActive, mode.value] as const,
|
|
213
|
+
() => void nextTick(recalcFold),
|
|
214
|
+
{ deep: false },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
function openWideMore() {
|
|
218
|
+
const r = wideMoreBtnEl.value?.getBoundingClientRect();
|
|
219
|
+
wideMoreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
|
|
220
|
+
}
|
|
221
|
+
/* === /ui-fix ========================================================= */
|
|
222
|
+
|
|
155
223
|
function fire(key: string) {
|
|
156
224
|
emit('action', key);
|
|
157
225
|
}
|
|
@@ -291,7 +359,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
291
359
|
>
|
|
292
360
|
<!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
|
|
293
361
|
<template v-if="!narrow">
|
|
294
|
-
<div class="fe-toolbar__primary">
|
|
362
|
+
<div ref="primaryEl" class="fe-toolbar__primary">
|
|
295
363
|
<button
|
|
296
364
|
v-if="canGoUp"
|
|
297
365
|
type="button"
|
|
@@ -315,10 +383,10 @@ function onMoreSelect(a: ContextAction) {
|
|
|
315
383
|
</button>
|
|
316
384
|
|
|
317
385
|
<button
|
|
318
|
-
v-for="a in
|
|
386
|
+
v-for="a in visibleToolbarItems"
|
|
319
387
|
:key="a.key"
|
|
320
388
|
type="button"
|
|
321
|
-
class="fe-btn"
|
|
389
|
+
class="fe-btn fe-btn--fold"
|
|
322
390
|
:class="{ 'fe-btn--danger': a.danger, 'is-disabled': a.disabled }"
|
|
323
391
|
:disabled="a.disabled"
|
|
324
392
|
:title="a.label"
|
|
@@ -328,6 +396,36 @@ function onMoreSelect(a: ContextAction) {
|
|
|
328
396
|
<span class="fe-btn__label">{{ a.label }}</span>
|
|
329
397
|
</button>
|
|
330
398
|
|
|
399
|
+
<!-- ui-fix — sığmayan aksiyonlar tek satırı korumak için ⋯ menüsüne
|
|
400
|
+
katlanır (arama/görünüm kontrolleri artık alt satıra düşmez). -->
|
|
401
|
+
<button
|
|
402
|
+
v-if="overflowToolbarItems.length > 0"
|
|
403
|
+
ref="wideMoreBtnEl"
|
|
404
|
+
type="button"
|
|
405
|
+
class="fe-btn fe-btn--icon-only"
|
|
406
|
+
:title="t('toolbar.more')"
|
|
407
|
+
:aria-label="t('toolbar.more')"
|
|
408
|
+
aria-haspopup="menu"
|
|
409
|
+
@click="openWideMore"
|
|
410
|
+
>
|
|
411
|
+
<span class="fe-icon">⋯</span>
|
|
412
|
+
</button>
|
|
413
|
+
|
|
414
|
+
<!-- Görünmez ölçüm şeridi: TÜM aksiyonlar her zaman burada render
|
|
415
|
+
edilir; katlama hesabı gerçek genişliklerden yapılır. -->
|
|
416
|
+
<div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
|
|
417
|
+
<button
|
|
418
|
+
v-for="a in toolbarItems"
|
|
419
|
+
:key="'m-' + a.key"
|
|
420
|
+
type="button"
|
|
421
|
+
class="fe-btn"
|
|
422
|
+
tabindex="-1"
|
|
423
|
+
>
|
|
424
|
+
<span class="fe-icon">{{ a.icon }}</span>
|
|
425
|
+
<span class="fe-btn__label">{{ a.label }}</span>
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
331
429
|
<button
|
|
332
430
|
v-if="pasteEnabled && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
|
|
333
431
|
type="button"
|
|
@@ -589,5 +687,16 @@ function onMoreSelect(a: ContextAction) {
|
|
|
589
687
|
@select="onMoreSelect"
|
|
590
688
|
/>
|
|
591
689
|
</template>
|
|
690
|
+
|
|
691
|
+
<!-- ui-fix — wide modda katlanan aksiyonların ⋯ menüsü -->
|
|
692
|
+
<ContextMenu
|
|
693
|
+
v-if="!narrow"
|
|
694
|
+
ref="wideMoreRef"
|
|
695
|
+
:locale="locale"
|
|
696
|
+
:theme="theme || 'auto'"
|
|
697
|
+
:sheet="false"
|
|
698
|
+
:actions="overflowToolbarItems"
|
|
699
|
+
@select="(a) => fire(a.key)"
|
|
700
|
+
/>
|
|
592
701
|
</div>
|
|
593
702
|
</template>
|
|
@@ -341,6 +341,12 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
|
|
|
341
341
|
const root = rootEl.value;
|
|
342
342
|
if (!root) return;
|
|
343
343
|
|
|
344
|
+
/* ui-fix — while a context menu is open, global shortcuts stay quiet
|
|
345
|
+
* (except Esc, which the menu scopes itself). Otherwise e.g. Delete
|
|
346
|
+
* opens a modal UNDER the menu backdrop and the UI wedges: the
|
|
347
|
+
* backdrop intercepts every click on the new dialog. */
|
|
348
|
+
if (e.key !== 'Escape' && document.querySelector('.fe-ctx-backdrop')) return;
|
|
349
|
+
|
|
344
350
|
// Skip when the event originates inside a form control — don't
|
|
345
351
|
// want `Delete` while editing a filename, `/` while typing in the
|
|
346
352
|
// search box, etc. Escape always goes through so modals can close.
|
|
@@ -30,6 +30,9 @@ import type { ViewMode } from '../types/FileNode';
|
|
|
30
30
|
/** Per-tab split state — the secondary pane's own location. */
|
|
31
31
|
export interface TabSplit {
|
|
32
32
|
path: string;
|
|
33
|
+
/** ui-fix — the pane's OWN view mode (list/grid/gallery); undefined
|
|
34
|
+
* inherits the main panel's mode at split time. */
|
|
35
|
+
viewMode?: ViewMode;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface TabState {
|
|
@@ -82,10 +85,16 @@ export function useTabs(opts: UseTabsOptions) {
|
|
|
82
85
|
// gallery wave) must survive a round-trip through an older schema.
|
|
83
86
|
const vm =
|
|
84
87
|
typeof t.viewMode === 'string' && t.viewMode ? (t.viewMode as ViewMode) : 'list';
|
|
85
|
-
const rawSplit = t.split as { path?: unknown } | null | undefined;
|
|
88
|
+
const rawSplit = t.split as { path?: unknown; viewMode?: unknown } | null | undefined;
|
|
86
89
|
const split =
|
|
87
90
|
rawSplit && typeof rawSplit === 'object' && typeof rawSplit.path === 'string'
|
|
88
|
-
? {
|
|
91
|
+
? {
|
|
92
|
+
path: rawSplit.path,
|
|
93
|
+
viewMode:
|
|
94
|
+
typeof rawSplit.viewMode === 'string' && rawSplit.viewMode
|
|
95
|
+
? (rawSplit.viewMode as ViewMode)
|
|
96
|
+
: undefined,
|
|
97
|
+
}
|
|
89
98
|
: null;
|
|
90
99
|
clean.push({
|
|
91
100
|
id: typeof t.id === 'string' && t.id ? t.id : makeId(),
|