@brftech/filex-core 0.31.0 → 0.33.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/README.md +26 -2
- package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
- package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
- package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
- package/dist/DrawioViewer-BNALOB04.js.map +1 -0
- package/dist/filex-core.js +9346 -8069
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +49 -49
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +235 -16
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +776 -246
- package/src/components/CommandPalette.vue +21 -5
- package/src/components/ConnectionsPanel.vue +2 -1
- package/src/components/E2eRecoveryUnlockModal.vue +29 -4
- package/src/components/FilterBar.vue +244 -0
- package/src/components/GalleryView.vue +12 -1
- package/src/components/GridView.vue +48 -5
- package/src/components/InspectorPanel.vue +231 -8
- package/src/components/ListView.vue +12 -3
- package/src/components/NFSExportsPanel.vue +2 -1
- package/src/components/PendingOpsTray.vue +1 -1
- package/src/components/PresenceBar.vue +3 -2
- package/src/components/S3KeysPanel.vue +2 -1
- package/src/components/SSHKeysPanel.vue +2 -1
- package/src/components/SecondaryPane.vue +28 -13
- package/src/components/SideNav.vue +161 -1
- package/src/components/StarButton.vue +2 -1
- package/src/components/TabBar.vue +2 -2
- package/src/components/ThemeGallery.vue +1 -1
- package/src/components/TokensPanel.vue +2 -1
- package/src/components/Toolbar.vue +244 -58
- package/src/components/ViewSwitcher.vue +81 -0
- package/src/composables/useFileApi.ts +49 -2
- package/src/composables/useLocale.ts +1 -1
- package/src/composables/useOperations.ts +3 -3
- package/src/composables/usePendingOps.ts +1 -1
- package/src/composables/useTabs.ts +1 -1
- package/src/composables/useUploadChunked.ts +39 -7
- package/src/index.ts +8 -2
- package/src/lib/e2ecrypto.ts +191 -3
- package/src/lib/fileFilters.ts +143 -0
- package/src/lib/listing.ts +47 -0
- package/src/lib/themes.ts +7 -7
- package/src/lib/transfer.ts +6 -6
- package/src/locales/en.ts +115 -0
- package/src/locales/index.ts +1 -0
- package/src/locales/resolve.ts +66 -0
- package/src/locales/tr.ts +116 -0
- package/src/modals/ConvertModal.vue +14 -12
- package/src/modals/NewFolderModal.vue +1 -1
- package/src/modals/PermissionsModal.vue +30 -13
- package/src/modals/PreviewModal.vue +9 -9
- package/src/styles/base.css +520 -26
- package/src/styles/variables.css +1 -1
- package/src/types/ExplorerConfig.ts +18 -2
- package/src/types/FileNode.ts +1 -1
- package/src/viewers/CsvViewer.vue +4 -4
- package/src/viewers/DrawioViewer.vue +2 -2
- package/dist/CsvViewer-DxujFEM3.js.map +0 -1
- package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Toolbar — selection-aware action row.
|
|
4
4
|
*
|
|
5
5
|
* Layout:
|
|
6
|
-
* - No selection → [📁
|
|
6
|
+
* - No selection → [📁 New Folder] [search | ⬆ ↻] [☰ ▦]
|
|
7
7
|
* - 1 file selected → [👁 ⬇ 🔗 ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
8
8
|
* - 1 folder selected → [↗ ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
9
9
|
* - Multi selection → [✂ ❐ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
@@ -15,6 +15,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
15
15
|
import type { ViewMode } from '../types/FileNode';
|
|
16
16
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
17
17
|
import ContextMenu, { type ContextAction } from './ContextMenu.vue';
|
|
18
|
+
import ViewSwitcher from './ViewSwitcher.vue';
|
|
18
19
|
import { useLocale } from '../composables/useLocale';
|
|
19
20
|
|
|
20
21
|
export type SelectionMode = 'none' | 'single-file' | 'single-dir' | 'multi';
|
|
@@ -90,6 +91,26 @@ const props = defineProps<{
|
|
|
90
91
|
* "power user tool", and gallery is the one nobody outside photos asks for.
|
|
91
92
|
*/
|
|
92
93
|
viewModes?: ViewMode[];
|
|
94
|
+
/* === surucu:d1 — the Drive shell ==================================== */
|
|
95
|
+
/**
|
|
96
|
+
* Which header this is.
|
|
97
|
+
*
|
|
98
|
+
* 'classic' (default) — the row this toolbar has always been.
|
|
99
|
+
* 'drive' — `uiProfile: 'drive'`: one search field across the
|
|
100
|
+
* width with its ⌘K/Ctrl+K hint, and the controls
|
|
101
|
+
* that belong to a *listing* rather than to the app
|
|
102
|
+
* (view switcher, details toggle) moved down onto the
|
|
103
|
+
* breadcrumb row, where the mockups put them.
|
|
104
|
+
*
|
|
105
|
+
* ⚠ Nothing is DELETED by 'drive'. Density, theme, the shortcut editor, the
|
|
106
|
+
* tour and both other view modes are all in the "⋯" menu this variant
|
|
107
|
+
* renders — the same list the narrow layout has used since bag:b4. A control
|
|
108
|
+
* that is on screen in one profile and unreachable in another is a capability
|
|
109
|
+
* split, not a preset.
|
|
110
|
+
*/
|
|
111
|
+
shell?: 'classic' | 'drive';
|
|
112
|
+
/** Folder name for the drive field's placeholder ("Search in Photos"). */
|
|
113
|
+
scopeLabel?: string;
|
|
93
114
|
}>();
|
|
94
115
|
|
|
95
116
|
const emit = defineEmits<{
|
|
@@ -106,6 +127,8 @@ const emit = defineEmits<{
|
|
|
106
127
|
(e: 'toggle-nav'): void /* gezinti:g1 */;
|
|
107
128
|
(e: 'open-theme'): void /* wiring:c1 — tema galerisi */;
|
|
108
129
|
(e: 'open-shortcut-settings'): void /* wiring:c2 */;
|
|
130
|
+
/* surucu:d1 — escalate this query into the command palette (⌘K/Ctrl+K). */
|
|
131
|
+
(e: 'open-palette', query: string): void;
|
|
109
132
|
}>();
|
|
110
133
|
|
|
111
134
|
// Density toggle — the toolbar owns the persisted preference; the parent
|
|
@@ -294,16 +317,14 @@ function offersView(v: ViewMode): boolean {
|
|
|
294
317
|
return !props.viewModes || props.viewModes.includes(v);
|
|
295
318
|
}
|
|
296
319
|
|
|
297
|
-
|
|
320
|
+
/* surucu:d1 — the tail of the "⋯" menu on its own: everything that is a
|
|
321
|
+
* SETTING of the explorer rather than an action on a file. The narrow layout
|
|
322
|
+
* shows it after the write/selection actions (unchanged); the drive header
|
|
323
|
+
* shows only this half, because in that layout the selection actions are
|
|
324
|
+
* already on screen as buttons and a menu that repeats them is a second door
|
|
325
|
+
* to the same room. */
|
|
326
|
+
const utilityActions = computed<ContextAction[]>(() => {
|
|
298
327
|
const list: ContextAction[] = [];
|
|
299
|
-
const writable =
|
|
300
|
-
!props.trashActive && !props.atVirtualRoot && props.canWrite !== false;
|
|
301
|
-
if (mode.value === 'none' && writable) {
|
|
302
|
-
list.push({ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' });
|
|
303
|
-
if (props.pasteEnabled) list.push({ key: 'paste', label: t('ctx.paste'), icon: '📋' });
|
|
304
|
-
}
|
|
305
|
-
list.push(...toolbarItems.value);
|
|
306
|
-
if (list.length) list.push({ divider: true, key: 'bag-sep', label: '' });
|
|
307
328
|
list.push({ key: 'refresh', label: t('toolbar.refresh'), icon: '↻' });
|
|
308
329
|
list.push({
|
|
309
330
|
key: 'density',
|
|
@@ -313,8 +334,8 @@ const moreActions = computed<ContextAction[]>(() => {
|
|
|
313
334
|
: t('toolbar.density.compact'),
|
|
314
335
|
icon: '⇅',
|
|
315
336
|
});
|
|
316
|
-
/* wiring:d2 —
|
|
317
|
-
(list/grid/gallery);
|
|
337
|
+
/* wiring:d2 — narrow-mode ⋯ menu: BOTH inactive views are listed
|
|
338
|
+
(list/grid/gallery); the old single toggle fell short with three modes. */
|
|
318
339
|
if (props.viewMode !== 'list' && offersView('list')) list.push({ key: 'view-list', label: t('toolbar.view.list'), icon: '☰' });
|
|
319
340
|
if (props.viewMode !== 'grid' && offersView('grid')) list.push({ key: 'view-grid', label: t('toolbar.view.grid'), icon: '▦' });
|
|
320
341
|
if (props.viewMode !== 'gallery' && offersView('gallery')) list.push({ key: 'view-gallery', label: t('toolbar.view.gallery'), icon: '▣' });
|
|
@@ -326,13 +347,69 @@ const moreActions = computed<ContextAction[]>(() => {
|
|
|
326
347
|
}
|
|
327
348
|
/* koru:k1 — inspector toggle also reachable from the narrow overflow menu */
|
|
328
349
|
list.push({ key: 'inspector', label: t('toolbar.inspector'), icon: 'ℹ' });
|
|
329
|
-
/* wiring:c1 —
|
|
350
|
+
/* wiring:c1 — the theme gallery is opened from the ⋯ menu in narrow mode too */
|
|
330
351
|
list.push({ key: 'theme', label: t('theme.menu'), icon: '🎨' });
|
|
331
352
|
list.push({ key: 'shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨' }) /* wiring:c2 */;
|
|
332
353
|
list.push({ key: 'tour', label: t('tour.restart'), icon: '🎓' }); /* wiring:c4 */
|
|
333
354
|
return list;
|
|
334
355
|
});
|
|
335
356
|
|
|
357
|
+
const moreActions = computed<ContextAction[]>(() => {
|
|
358
|
+
const list: ContextAction[] = [];
|
|
359
|
+
const writable =
|
|
360
|
+
!props.trashActive && !props.atVirtualRoot && props.canWrite !== false;
|
|
361
|
+
if (mode.value === 'none' && writable) {
|
|
362
|
+
list.push({ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' });
|
|
363
|
+
if (props.pasteEnabled) list.push({ key: 'paste', label: t('ctx.paste'), icon: '📋' });
|
|
364
|
+
}
|
|
365
|
+
list.push(...toolbarItems.value);
|
|
366
|
+
if (list.length) list.push({ divider: true, key: 'bag-sep', label: '' });
|
|
367
|
+
list.push(...utilityActions.value);
|
|
368
|
+
return list;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/* === surucu:d1 — the drive header's search field ======================
|
|
372
|
+
* ONE field, and it is the one the toolbar already had: typing here sets
|
|
373
|
+
* `searchQuery`, which the explorer answers with
|
|
374
|
+
* `?action=search&filter=…` — a real search of the folder you are standing in,
|
|
375
|
+
* exactly as before.
|
|
376
|
+
*
|
|
377
|
+
* The ⌘K chip beside it is the ESCALATION, not a second box: it opens the
|
|
378
|
+
* command palette carrying whatever has been typed, and the palette is where
|
|
379
|
+
* "everywhere" lives (the global endpoint, saved searches, and the commands).
|
|
380
|
+
* That is why the hint can sit on this field honestly — Ctrl+K from here does
|
|
381
|
+
* what the hint says, and Ctrl+K from anywhere else still opens the palette
|
|
382
|
+
* as it has since cila:c.
|
|
383
|
+
*/
|
|
384
|
+
const macLike =
|
|
385
|
+
typeof navigator !== 'undefined' &&
|
|
386
|
+
/mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '');
|
|
387
|
+
const paletteCombo = computed(() => (macLike ? '⌘ K' : 'Ctrl K'));
|
|
388
|
+
|
|
389
|
+
const drivePlaceholder = computed(() =>
|
|
390
|
+
props.scopeLabel
|
|
391
|
+
? t('drive.search.placeholder', { scope: props.scopeLabel })
|
|
392
|
+
: t('drive.search.placeholder_all'),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
/** ⌘/Ctrl+K typed INSIDE the field. The global shortcut handler ignores
|
|
396
|
+
* keystrokes aimed at an input — which is correct, and it would otherwise
|
|
397
|
+
* make the hint on this very field a lie. */
|
|
398
|
+
function onSearchKeydown(ev: KeyboardEvent) {
|
|
399
|
+
if ((ev.ctrlKey || ev.metaKey) && (ev.key === 'k' || ev.key === 'K')) {
|
|
400
|
+
ev.preventDefault();
|
|
401
|
+
ev.stopPropagation();
|
|
402
|
+
emit('open-palette', localSearch.value);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* surucu:d1 — the drive header renders the selection actions as buttons, so
|
|
407
|
+
* its "⋯" carries only the settings half; the narrow layout renders no action
|
|
408
|
+
* buttons at all and still needs the whole list. */
|
|
409
|
+
const menuActions = computed<ContextAction[]>(() =>
|
|
410
|
+
props.shell === 'drive' && !props.narrow ? utilityActions.value : moreActions.value,
|
|
411
|
+
);
|
|
412
|
+
|
|
336
413
|
function openMore() {
|
|
337
414
|
const r = moreBtnEl.value?.getBoundingClientRect();
|
|
338
415
|
moreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 }, []);
|
|
@@ -387,6 +464,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
387
464
|
:class="{
|
|
388
465
|
'fe-toolbar--narrow': narrow /* bag:b4 */,
|
|
389
466
|
'fe-toolbar--searching': narrow && searchOpen /* bag:b4 */,
|
|
467
|
+
'fe-toolbar--drive': shell === 'drive' /* surucu:d1 */,
|
|
390
468
|
}"
|
|
391
469
|
>
|
|
392
470
|
<!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
|
|
@@ -433,8 +511,11 @@ function onMoreSelect(a: ContextAction) {
|
|
|
433
511
|
<span class="fe-icon">↑</span>
|
|
434
512
|
</button>
|
|
435
513
|
|
|
514
|
+
<!-- surucu:d1 — in the drive shell the panel's "+ New" menu is the one
|
|
515
|
+
primary action; a second New Folder button here would be a second
|
|
516
|
+
front door to the same modal. -->
|
|
436
517
|
<button
|
|
437
|
-
v-if="mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
|
|
518
|
+
v-if="shell !== 'drive' && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
|
|
438
519
|
type="button"
|
|
439
520
|
class="fe-btn fe-btn--primary"
|
|
440
521
|
:title="t('toolbar.new_folder')"
|
|
@@ -458,8 +539,8 @@ function onMoreSelect(a: ContextAction) {
|
|
|
458
539
|
<span class="fe-btn__label">{{ a.label }}</span>
|
|
459
540
|
</button>
|
|
460
541
|
|
|
461
|
-
<!-- ui-fix —
|
|
462
|
-
|
|
542
|
+
<!-- ui-fix — actions that don't fit fold into the ⋯ menu so the row stays
|
|
543
|
+
a single line (search/view controls no longer drop to a second row). -->
|
|
463
544
|
<button
|
|
464
545
|
v-if="overflowToolbarItems.length > 0"
|
|
465
546
|
ref="wideMoreBtnEl"
|
|
@@ -473,8 +554,8 @@ function onMoreSelect(a: ContextAction) {
|
|
|
473
554
|
<span class="fe-icon">⋯</span>
|
|
474
555
|
</button>
|
|
475
556
|
|
|
476
|
-
<!--
|
|
477
|
-
|
|
557
|
+
<!-- Invisible measuring strip: EVERY action is always rendered here, so
|
|
558
|
+
the fold calculation runs off real measured widths. -->
|
|
478
559
|
<div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
|
|
479
560
|
<button
|
|
480
561
|
v-for="a in toolbarItems"
|
|
@@ -502,8 +583,45 @@ function onMoreSelect(a: ContextAction) {
|
|
|
502
583
|
|
|
503
584
|
<div class="fe-toolbar__spacer" />
|
|
504
585
|
|
|
505
|
-
<div class="fe-toolbar__search-group">
|
|
506
|
-
|
|
586
|
+
<div class="fe-toolbar__search-group" :class="{ 'fe-toolbar__search-group--drive': shell === 'drive' }">
|
|
587
|
+
<!-- surucu:d1 — the drive shell's one search field: wide, in the header,
|
|
588
|
+
with the ⌘K chip that opens the palette carrying this query. -->
|
|
589
|
+
<div v-if="shell === 'drive'" class="fe-drivesearch" data-testid="drive-search">
|
|
590
|
+
<svg
|
|
591
|
+
class="fe-ficon fe-drivesearch__glass"
|
|
592
|
+
viewBox="0 0 24 24"
|
|
593
|
+
fill="none"
|
|
594
|
+
stroke="currentColor"
|
|
595
|
+
stroke-width="1.9"
|
|
596
|
+
stroke-linecap="round"
|
|
597
|
+
aria-hidden="true"
|
|
598
|
+
focusable="false"
|
|
599
|
+
>
|
|
600
|
+
<circle cx="11" cy="11" r="6.5" />
|
|
601
|
+
<path d="M16 16l4.5 4.5" />
|
|
602
|
+
</svg>
|
|
603
|
+
<input
|
|
604
|
+
ref="searchEl"
|
|
605
|
+
type="search"
|
|
606
|
+
class="fe-drivesearch__input"
|
|
607
|
+
:placeholder="drivePlaceholder"
|
|
608
|
+
:value="localSearch"
|
|
609
|
+
:aria-label="t('toolbar.search')"
|
|
610
|
+
@input="onSearchInput"
|
|
611
|
+
@keydown="onSearchKeydown"
|
|
612
|
+
/>
|
|
613
|
+
<button
|
|
614
|
+
type="button"
|
|
615
|
+
class="fe-drivesearch__kbd"
|
|
616
|
+
:title="t('drive.search.hint_title', { combo: paletteCombo })"
|
|
617
|
+
:aria-label="t('drive.search.hint_title', { combo: paletteCombo })"
|
|
618
|
+
data-testid="drive-search-palette"
|
|
619
|
+
@click="emit('open-palette', localSearch)"
|
|
620
|
+
>
|
|
621
|
+
{{ paletteCombo }}
|
|
622
|
+
</button>
|
|
623
|
+
</div>
|
|
624
|
+
<div v-else class="fe-search">
|
|
507
625
|
<input
|
|
508
626
|
ref="searchEl"
|
|
509
627
|
type="search"
|
|
@@ -515,7 +633,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
515
633
|
/>
|
|
516
634
|
</div>
|
|
517
635
|
<button
|
|
518
|
-
v-if="!atVirtualRoot && canWrite !== false"
|
|
636
|
+
v-if="shell !== 'drive' && !atVirtualRoot && canWrite !== false"
|
|
519
637
|
type="button"
|
|
520
638
|
class="fe-btn fe-btn--icon-only"
|
|
521
639
|
:title="t('toolbar.upload')"
|
|
@@ -533,9 +651,26 @@ function onMoreSelect(a: ContextAction) {
|
|
|
533
651
|
>
|
|
534
652
|
<span class="fe-icon" aria-hidden="true">↻</span>
|
|
535
653
|
</button>
|
|
654
|
+
<!-- surucu:d1 — density, theme, the shortcut editor, the tour and the
|
|
655
|
+
other view modes. Everything the drive header does not draw is one
|
|
656
|
+
click away here; nothing is removed from the build. -->
|
|
657
|
+
<button
|
|
658
|
+
v-if="shell === 'drive'"
|
|
659
|
+
ref="moreBtnEl"
|
|
660
|
+
type="button"
|
|
661
|
+
class="fe-btn fe-btn--icon-only"
|
|
662
|
+
:title="t('toolbar.more')"
|
|
663
|
+
:aria-label="t('toolbar.more')"
|
|
664
|
+
aria-haspopup="menu"
|
|
665
|
+
data-testid="drive-more"
|
|
666
|
+
@click="openMore"
|
|
667
|
+
>
|
|
668
|
+
<span class="fe-icon" aria-hidden="true">⋯</span>
|
|
669
|
+
</button>
|
|
536
670
|
</div>
|
|
537
671
|
|
|
538
672
|
<button
|
|
673
|
+
v-if="shell !== 'drive'"
|
|
539
674
|
type="button"
|
|
540
675
|
class="fe-btn fe-btn--icon-only fe-toolbar__density"
|
|
541
676
|
:class="{ 'is-active': density === 'compact' }"
|
|
@@ -573,8 +708,11 @@ function onMoreSelect(a: ContextAction) {
|
|
|
573
708
|
</svg>
|
|
574
709
|
</button>
|
|
575
710
|
|
|
576
|
-
<!-- koru:k1 — inspector (details panel) toggle
|
|
711
|
+
<!-- koru:k1 — inspector (details panel) toggle.
|
|
712
|
+
surucu:d1 — the drive shell renders it on the breadcrumb row instead,
|
|
713
|
+
beside the view switcher, where both belong to the listing. -->
|
|
577
714
|
<button
|
|
715
|
+
v-if="shell !== 'drive'"
|
|
578
716
|
type="button"
|
|
579
717
|
class="fe-btn fe-btn--icon-only fe-toolbar__inspector"
|
|
580
718
|
:class="{ 'is-active': inspectorOpen }"
|
|
@@ -601,6 +739,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
601
739
|
|
|
602
740
|
<!-- wiring:c1 — tema galerisi (palet ikonu) -->
|
|
603
741
|
<button
|
|
742
|
+
v-if="shell !== 'drive'"
|
|
604
743
|
type="button"
|
|
605
744
|
class="fe-btn fe-btn--icon-only fe-toolbar__theme"
|
|
606
745
|
:title="t('theme.menu')"
|
|
@@ -626,49 +765,96 @@ function onMoreSelect(a: ContextAction) {
|
|
|
626
765
|
</button>
|
|
627
766
|
<!-- /wiring:c1 -->
|
|
628
767
|
|
|
629
|
-
<
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
768
|
+
<ViewSwitcher
|
|
769
|
+
v-if="shell !== 'drive'"
|
|
770
|
+
:view-mode="viewMode"
|
|
771
|
+
:locale="locale"
|
|
772
|
+
:modes="viewModes"
|
|
773
|
+
@update:view-mode="emit('update:viewMode', $event)"
|
|
774
|
+
/>
|
|
775
|
+
</template>
|
|
776
|
+
|
|
777
|
+
<!-- surucu:d1 — narrow DRIVE layout: [☰] [ search ] [⋯].
|
|
778
|
+
The classic narrow row hides search behind a magnifier, which is the
|
|
779
|
+
right trade when the row also carries Up / Upload / actions. This shell
|
|
780
|
+
does not: "+ New" lives in the drawer, upload has the floating button
|
|
781
|
+
at this width already, and the whole point of the profile is that the
|
|
782
|
+
search field is the thing you see. So the field keeps the row, and only
|
|
783
|
+
the ⌘K chip goes (CSS) — a phone cannot press it, and the palette stays
|
|
784
|
+
in the "⋯" menu. -->
|
|
785
|
+
<template v-else-if="shell === 'drive'">
|
|
643
786
|
<button
|
|
787
|
+
v-if="navEnabled !== false"
|
|
644
788
|
type="button"
|
|
645
|
-
|
|
646
|
-
class="
|
|
647
|
-
:
|
|
648
|
-
|
|
649
|
-
:aria-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
@click="emit('update:viewMode', 'grid')"
|
|
789
|
+
class="fe-btn fe-btn--icon-only fe-toolbar__nav"
|
|
790
|
+
:class="{ 'is-active': navOpen }"
|
|
791
|
+
:aria-pressed="!!navOpen"
|
|
792
|
+
:title="t('toolbar.nav')"
|
|
793
|
+
:aria-label="t('toolbar.nav')"
|
|
794
|
+
data-testid="toolbar-nav"
|
|
795
|
+
@click="emit('toggle-nav')"
|
|
653
796
|
>
|
|
654
|
-
<
|
|
797
|
+
<svg
|
|
798
|
+
class="fe-ficon"
|
|
799
|
+
viewBox="0 0 24 24"
|
|
800
|
+
fill="none"
|
|
801
|
+
stroke="currentColor"
|
|
802
|
+
stroke-width="1.8"
|
|
803
|
+
stroke-linecap="round"
|
|
804
|
+
aria-hidden="true"
|
|
805
|
+
focusable="false"
|
|
806
|
+
>
|
|
807
|
+
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
|
808
|
+
<path d="M9.5 4.5v15" />
|
|
809
|
+
</svg>
|
|
655
810
|
</button>
|
|
656
|
-
|
|
811
|
+
<div class="fe-drivesearch" data-testid="drive-search">
|
|
812
|
+
<svg
|
|
813
|
+
class="fe-ficon fe-drivesearch__glass"
|
|
814
|
+
viewBox="0 0 24 24"
|
|
815
|
+
fill="none"
|
|
816
|
+
stroke="currentColor"
|
|
817
|
+
stroke-width="1.9"
|
|
818
|
+
stroke-linecap="round"
|
|
819
|
+
aria-hidden="true"
|
|
820
|
+
focusable="false"
|
|
821
|
+
>
|
|
822
|
+
<circle cx="11" cy="11" r="6.5" />
|
|
823
|
+
<path d="M16 16l4.5 4.5" />
|
|
824
|
+
</svg>
|
|
825
|
+
<input
|
|
826
|
+
ref="searchEl"
|
|
827
|
+
type="search"
|
|
828
|
+
class="fe-drivesearch__input"
|
|
829
|
+
:placeholder="drivePlaceholder"
|
|
830
|
+
:value="localSearch"
|
|
831
|
+
:aria-label="t('toolbar.search')"
|
|
832
|
+
@input="onSearchInput"
|
|
833
|
+
@keydown="onSearchKeydown"
|
|
834
|
+
/>
|
|
835
|
+
<button
|
|
836
|
+
type="button"
|
|
837
|
+
class="fe-drivesearch__kbd"
|
|
838
|
+
:title="t('drive.search.hint_title', { combo: paletteCombo })"
|
|
839
|
+
:aria-label="t('drive.search.hint_title', { combo: paletteCombo })"
|
|
840
|
+
data-testid="drive-search-palette"
|
|
841
|
+
@click="emit('open-palette', localSearch)"
|
|
842
|
+
>
|
|
843
|
+
{{ paletteCombo }}
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
657
846
|
<button
|
|
847
|
+
ref="moreBtnEl"
|
|
658
848
|
type="button"
|
|
659
|
-
v-if="offersView('gallery')"
|
|
660
849
|
class="fe-btn fe-btn--icon-only"
|
|
661
|
-
:
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
@click="emit('update:viewMode', 'gallery')"
|
|
850
|
+
:title="t('toolbar.more')"
|
|
851
|
+
:aria-label="t('toolbar.more')"
|
|
852
|
+
aria-haspopup="menu"
|
|
853
|
+
data-testid="drive-more"
|
|
854
|
+
@click="openMore"
|
|
667
855
|
>
|
|
668
|
-
<span class="fe-icon"
|
|
856
|
+
<span class="fe-icon">⋯</span>
|
|
669
857
|
</button>
|
|
670
|
-
<!-- /wiring:d2 -->
|
|
671
|
-
</div>
|
|
672
858
|
</template>
|
|
673
859
|
|
|
674
860
|
<!-- bag:b4 — narrow layout: [↑?] ... [🔍] [⬆] [⋯], search expands full-width -->
|
|
@@ -778,12 +964,12 @@ function onMoreSelect(a: ContextAction) {
|
|
|
778
964
|
:locale="locale"
|
|
779
965
|
:theme="theme || 'auto'"
|
|
780
966
|
:sheet="coarse"
|
|
781
|
-
:actions="
|
|
967
|
+
:actions="menuActions"
|
|
782
968
|
@select="onMoreSelect"
|
|
783
969
|
/>
|
|
784
970
|
</template>
|
|
785
971
|
|
|
786
|
-
<!-- ui-fix —
|
|
972
|
+
<!-- ui-fix — ⋯ menu for the actions folded away in wide mode -->
|
|
787
973
|
<ContextMenu
|
|
788
974
|
v-if="!narrow"
|
|
789
975
|
ref="wideMoreRef"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ViewSwitcher — the list / grid / gallery toggle.
|
|
4
|
+
*
|
|
5
|
+
* Lifted out of Toolbar.vue unchanged (same classes, same glyphs, same
|
|
6
|
+
* `role="tablist"` semantics) because the `drive` profile puts it on the
|
|
7
|
+
* breadcrumb row instead of in the header, and two copies of a switcher is
|
|
8
|
+
* how the two rows end up offering different modes. One component, mounted
|
|
9
|
+
* in whichever row the profile asks for.
|
|
10
|
+
*
|
|
11
|
+
* `modes` absent = all three, so every embedder that predates `uiProfile`
|
|
12
|
+
* keeps the switcher it had.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ No `<style>` block — package CSS lives in `styles/base.css` (a scoped
|
|
15
|
+
* block's `data-v` hash does not match in the web-component build).
|
|
16
|
+
*/
|
|
17
|
+
import { useLocale } from '../composables/useLocale';
|
|
18
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
19
|
+
import type { ViewMode } from '../types/FileNode';
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
viewMode: ViewMode;
|
|
23
|
+
locale: LocaleCode;
|
|
24
|
+
modes?: ViewMode[];
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{ (e: 'update:viewMode', v: ViewMode): void }>();
|
|
28
|
+
|
|
29
|
+
const { t } = useLocale(() => props.locale);
|
|
30
|
+
|
|
31
|
+
function offers(v: ViewMode): boolean {
|
|
32
|
+
return !props.modes || props.modes.includes(v);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="fe-toolbar__view" role="tablist" :aria-label="t('toolbar.view_label')">
|
|
38
|
+
<button
|
|
39
|
+
v-if="offers('list')"
|
|
40
|
+
type="button"
|
|
41
|
+
class="fe-btn fe-btn--icon-only"
|
|
42
|
+
:class="{ 'is-active': viewMode === 'list' }"
|
|
43
|
+
role="tab"
|
|
44
|
+
:aria-selected="viewMode === 'list'"
|
|
45
|
+
:title="t('toolbar.view.list')"
|
|
46
|
+
:aria-label="t('toolbar.view.list')"
|
|
47
|
+
data-testid="view-list"
|
|
48
|
+
@click="emit('update:viewMode', 'list')"
|
|
49
|
+
>
|
|
50
|
+
<span class="fe-icon" aria-hidden="true">☰</span>
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
v-if="offers('grid')"
|
|
54
|
+
type="button"
|
|
55
|
+
class="fe-btn fe-btn--icon-only"
|
|
56
|
+
:class="{ 'is-active': viewMode === 'grid' }"
|
|
57
|
+
role="tab"
|
|
58
|
+
:aria-selected="viewMode === 'grid'"
|
|
59
|
+
:title="t('toolbar.view.grid')"
|
|
60
|
+
:aria-label="t('toolbar.view.grid')"
|
|
61
|
+
data-testid="view-grid"
|
|
62
|
+
@click="emit('update:viewMode', 'grid')"
|
|
63
|
+
>
|
|
64
|
+
<span class="fe-icon" aria-hidden="true">▦</span>
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
v-if="offers('gallery')"
|
|
68
|
+
type="button"
|
|
69
|
+
class="fe-btn fe-btn--icon-only"
|
|
70
|
+
:class="{ 'is-active': viewMode === 'gallery' }"
|
|
71
|
+
role="tab"
|
|
72
|
+
:aria-selected="viewMode === 'gallery'"
|
|
73
|
+
:title="t('toolbar.view.gallery')"
|
|
74
|
+
:aria-label="t('toolbar.view.gallery')"
|
|
75
|
+
data-testid="view-gallery"
|
|
76
|
+
@click="emit('update:viewMode', 'gallery')"
|
|
77
|
+
>
|
|
78
|
+
<span class="fe-icon" aria-hidden="true">▣</span>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { ExplorerConfig, AuthConfig, EndpointMap } from '../types/ExplorerConfig';
|
|
25
|
+
import { resolveLocale } from '../locales/resolve';
|
|
25
26
|
import type {
|
|
26
27
|
FileNode,
|
|
27
28
|
ShareInfo,
|
|
@@ -77,6 +78,14 @@ export interface Grant {
|
|
|
77
78
|
inherited?: boolean;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/** surucu:d1 — `GET /api/files/quota/me` (quota.Snapshot). */
|
|
82
|
+
export interface QuotaSnapshot {
|
|
83
|
+
used_bytes: number;
|
|
84
|
+
quota_bytes: number;
|
|
85
|
+
percent_used: number;
|
|
86
|
+
unlimited: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
export interface PermissionsResponse {
|
|
81
90
|
path: string;
|
|
82
91
|
storage_rbac: boolean;
|
|
@@ -328,9 +337,10 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
328
337
|
|
|
329
338
|
// Map an HTTP status to a short, human-readable message in the explorer's
|
|
330
339
|
// locale. The raw JSON body is attached as `.detail` for debugging but never
|
|
331
|
-
// shown in the toast (Ada: "
|
|
340
|
+
// shown in the toast (Ada, translated from Turkish: "when it gives a 404/403
|
|
341
|
+
// or whatever, I see raw json").
|
|
332
342
|
function statusMessage(status: number): string {
|
|
333
|
-
const tr = (config.locale
|
|
343
|
+
const tr = resolveLocale(config.locale) !== 'en';
|
|
334
344
|
const m: Record<number, [string, string]> = {
|
|
335
345
|
400: ['Geçersiz istek', 'Bad request'],
|
|
336
346
|
401: ['Oturum gerekli, tekrar giriş yapın', 'Sign-in required'],
|
|
@@ -457,6 +467,31 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
457
467
|
return Array.isArray(data?.results) ? data.results : [];
|
|
458
468
|
}
|
|
459
469
|
|
|
470
|
+
/* === surucu:d1 — the signed-in person's storage line ==================
|
|
471
|
+
* `GET /api/files/quota/me` → `{used_bytes, quota_bytes, percent_used,
|
|
472
|
+
* unlimited}` (internal/quota/service.go `Snapshot`). Derived from the
|
|
473
|
+
* manager endpoint the same way permissions and search are, so a proxy that
|
|
474
|
+
* forwards /api/files/* keeps working.
|
|
475
|
+
*
|
|
476
|
+
* ⚠ PER-USER, never per-storage: usage is `SUM(nodes.size) WHERE owner_id=me`
|
|
477
|
+
* and there is no per-provider quota. Anything labelling this figure with a
|
|
478
|
+
* drive's name is describing a number the server did not send.
|
|
479
|
+
*
|
|
480
|
+
* ⚠ Returns null rather than throwing on ANY failure — a server without the
|
|
481
|
+
* route (404), an app token with no person behind it (403) or an older build
|
|
482
|
+
* must leave the panel exactly as it was, not put an error where a status
|
|
483
|
+
* line goes.
|
|
484
|
+
*/
|
|
485
|
+
async function quotaMe(): Promise<QuotaSnapshot | null> {
|
|
486
|
+
const base = endpoints.manager.replace(/\/manager(\?.*)?$/, '/quota/me');
|
|
487
|
+
try {
|
|
488
|
+
const q = await jsonFetch<QuotaSnapshot>(base);
|
|
489
|
+
return q && typeof q.used_bytes === 'number' ? q : null;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
460
495
|
async function subfolders(path: string): Promise<{ folders: FileNode[] }> {
|
|
461
496
|
return jsonFetch<{ folders: FileNode[] }>(managerUrl('subfolders', { path }));
|
|
462
497
|
}
|
|
@@ -627,11 +662,22 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
627
662
|
*/
|
|
628
663
|
async function fetchBlob(
|
|
629
664
|
path: string,
|
|
665
|
+
opts: { fresh?: boolean } = {},
|
|
630
666
|
): Promise<{ url: string; blob: Blob; mime: string }> {
|
|
631
667
|
const headers = await authHeaders();
|
|
632
668
|
const res = await fetch(previewUrl(path), {
|
|
633
669
|
headers,
|
|
634
670
|
credentials: credentialsMode(),
|
|
671
|
+
// ⚠ The preview endpoint answers `Cache-Control: private, max-age=60`,
|
|
672
|
+
// which is right for the thing it was built for — a viewer re-opening
|
|
673
|
+
// an image — and wrong for anything the client treats as CONTROL data.
|
|
674
|
+
// `.filex-e2e.json` is control data: after the client rewrites it
|
|
675
|
+
// (recovery upgrade, escrow slot, escrow refusal) the next read inside
|
|
676
|
+
// that minute came back PRE-write, so the folder looked un-upgraded
|
|
677
|
+
// and the question filex had just been answered was asked again.
|
|
678
|
+
// Measured 2026-09-05 in a real browser: decline the escrow offer,
|
|
679
|
+
// reopen the folder, and the offer was back.
|
|
680
|
+
cache: opts.fresh ? 'no-store' : 'default',
|
|
635
681
|
});
|
|
636
682
|
if (!res.ok) {
|
|
637
683
|
const text = await res.text().catch(() => '');
|
|
@@ -885,6 +931,7 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
885
931
|
index,
|
|
886
932
|
search,
|
|
887
933
|
globalSearch /* bul:s3 */,
|
|
934
|
+
quotaMe /* surucu:d1 */,
|
|
888
935
|
subfolders,
|
|
889
936
|
newFolder,
|
|
890
937
|
rename,
|
|
@@ -12,7 +12,7 @@ import { messages } from '../locales';
|
|
|
12
12
|
export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
|
|
13
13
|
const lookup = computed(() => {
|
|
14
14
|
const code = typeof localeRef === 'function' ? localeRef() : localeRef.value;
|
|
15
|
-
return messages[code] ?? messages.
|
|
15
|
+
return messages[code] ?? messages.en;
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
function t(key: string, vars: Record<string, string | number> = {}): string {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* are registered per row so the panel can route user intent back to the
|
|
15
15
|
* owning surface without the store knowing any transport details.
|
|
16
16
|
*
|
|
17
|
-
* Lifecycle rules (per the
|
|
17
|
+
* Lifecycle rules (per the Operations Center contract):
|
|
18
18
|
* - running rows live in `active`;
|
|
19
|
-
* - done/aborted rows linger in `active` for DONE_LINGER_MS ("
|
|
19
|
+
* - done/aborted rows linger in `active` for DONE_LINGER_MS ("done"
|
|
20
20
|
* flash) then move to `history` (session-only, capped);
|
|
21
21
|
* - error rows are STICKY: they stay in `active` until the user
|
|
22
22
|
* dismisses or retries them — even after the source list swept them
|
|
@@ -39,7 +39,7 @@ export interface OperationInput {
|
|
|
39
39
|
percent: number | null;
|
|
40
40
|
status: OperationStatus;
|
|
41
41
|
error?: string | null;
|
|
42
|
-
/** Queue op accepted but not started yet ("
|
|
42
|
+
/** Queue op accepted but not started yet ("Queued"). */
|
|
43
43
|
queued?: boolean;
|
|
44
44
|
/** Progress counters for queue ops (3/5 items). */
|
|
45
45
|
doneCount?: number;
|