@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
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { computed, ref } from 'vue';
|
|
32
32
|
import { useLocale } from '../composables/useLocale';
|
|
33
|
-
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
33
|
+
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
34
|
+
import ContextMenu, { type ContextAction } from './ContextMenu.vue';
|
|
34
35
|
|
|
35
36
|
/** The virtual listings the panel can open. '' = an ordinary folder. */
|
|
36
37
|
export type NavView = '' | 'recent' | 'starred' | 'shared' | 'trash' | 'tag';
|
|
@@ -98,6 +99,37 @@ const props = defineProps<{
|
|
|
98
99
|
/** RBAC/root state — false hides the write affordances. */
|
|
99
100
|
canWrite?: boolean;
|
|
100
101
|
locale: LocaleCode;
|
|
102
|
+
/* === surucu:d1 — the Drive shell ==================================== */
|
|
103
|
+
/**
|
|
104
|
+
* Fold the primary actions into ONE "+ New" menu (`uiProfile: 'drive'`).
|
|
105
|
+
*
|
|
106
|
+
* Absent/false keeps the two-button block every other profile has had since
|
|
107
|
+
* v0.30.1 — Upload as the primary, New folder one step quieter — so this is
|
|
108
|
+
* additive and nothing that mounts the package today moves.
|
|
109
|
+
*
|
|
110
|
+
* ⚠ The menu holds what the explorer can ALREADY do: upload files, make a
|
|
111
|
+
* folder (the modal offers the encrypted variant from inside itself), and
|
|
112
|
+
* ask somebody else for files. There is deliberately no "Upload folder": the
|
|
113
|
+
* upload path takes a flat `File[]` and would have to create the intermediate
|
|
114
|
+
* directories itself, and an entry that quietly flattens someone's folder into
|
|
115
|
+
* one heap is worse than an entry that is not there.
|
|
116
|
+
*/
|
|
117
|
+
newMenu?: boolean;
|
|
118
|
+
/** Offer "Request files" in that menu — a folder we may write to and share. */
|
|
119
|
+
canRequestFiles?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The signed-in person's storage line, from `GET /api/files/quota/me`. Null
|
|
122
|
+
* (the default) renders nothing at all.
|
|
123
|
+
*
|
|
124
|
+
* ⚠ It is a PER-USER figure, not this storage's — `quota.Snapshot` sums
|
|
125
|
+
* `nodes.size WHERE owner_id = me`, and there is no per-provider quota
|
|
126
|
+
* (internal/quota/service.go). The mockup labels it with the drive's name;
|
|
127
|
+
* that would be a lie about which number this is, so the label says
|
|
128
|
+
* "Storage" and the drive name stays out of it.
|
|
129
|
+
*/
|
|
130
|
+
quota?: { used: number; total: number; unlimited: boolean } | null;
|
|
131
|
+
/** Resolved theme — the teleported New menu leaves the `.fe` variable scope. */
|
|
132
|
+
theme?: ThemeMode;
|
|
101
133
|
}>();
|
|
102
134
|
|
|
103
135
|
const emit = defineEmits<{
|
|
@@ -110,6 +142,8 @@ const emit = defineEmits<{
|
|
|
110
142
|
(e: 'new-folder'): void;
|
|
111
143
|
(e: 'open-connections'): void;
|
|
112
144
|
(e: 'open-tokens'): void;
|
|
145
|
+
/* surucu:d1 — "Request files": the access modal on THIS folder, drop tab. */
|
|
146
|
+
(e: 'request-files'): void;
|
|
113
147
|
/** Drawer scrim / Esc — narrow mode only. */
|
|
114
148
|
(e: 'close'): void;
|
|
115
149
|
}>();
|
|
@@ -168,6 +202,74 @@ const hiddenTagCount = computed(() => Math.max(0, allTags.value.length - visible
|
|
|
168
202
|
* answer is still in flight. */
|
|
169
203
|
const showTags = computed(() => !!props.tagsLoaded || allTags.value.length > 0);
|
|
170
204
|
|
|
205
|
+
/* === surucu:d1 — the "+ New" menu ====================================
|
|
206
|
+
* One primary action instead of two buttons, the shape the reporter drew and
|
|
207
|
+
* the shape Drive/FileRun/Nextcloud share. It is the SAME ContextMenu the
|
|
208
|
+
* right-click menu uses — teleported to <body>, so the panel's own scroll
|
|
209
|
+
* container cannot clip it, and one keyboard/focus behaviour for both. */
|
|
210
|
+
const newBtnEl = ref<HTMLElement | null>(null);
|
|
211
|
+
const newMenuRef = ref<InstanceType<typeof ContextMenu> | null>(null);
|
|
212
|
+
|
|
213
|
+
const newActions = computed<ContextAction[]>(() => {
|
|
214
|
+
const list: ContextAction[] = [
|
|
215
|
+
{ key: 'upload', label: t('drive.new.upload'), icon: '⬆', disabled: !writable.value },
|
|
216
|
+
{ key: 'new-folder', label: t('drive.new.folder'), icon: '📁', disabled: !writable.value },
|
|
217
|
+
];
|
|
218
|
+
// Only when there is a real folder to hang a drop link on. Rendered as a
|
|
219
|
+
// disabled row rather than dropped, so the menu does not change height
|
|
220
|
+
// between folders — a menu whose items move is a menu people misclick.
|
|
221
|
+
list.push({ divider: true, key: 'new-sep', label: '' });
|
|
222
|
+
list.push({
|
|
223
|
+
key: 'request-files',
|
|
224
|
+
label: t('drive.new.request'),
|
|
225
|
+
icon: '🔗',
|
|
226
|
+
disabled: !props.canRequestFiles,
|
|
227
|
+
});
|
|
228
|
+
return list;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
function openNewMenu() {
|
|
232
|
+
const r = newBtnEl.value?.getBoundingClientRect();
|
|
233
|
+
newMenuRef.value?.show(
|
|
234
|
+
{ clientX: r ? r.left : 0, clientY: r ? r.bottom + 6 : 0 } as MouseEvent,
|
|
235
|
+
[],
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function onNewSelect(a: ContextAction) {
|
|
240
|
+
if (a.key === 'upload') emit('upload');
|
|
241
|
+
else if (a.key === 'new-folder') emit('new-folder');
|
|
242
|
+
else if (a.key === 'request-files') emit('request-files');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* === surucu:d1 — the storage line =================================== */
|
|
246
|
+
function formatBytes(n: number): string {
|
|
247
|
+
if (!Number.isFinite(n) || n < 0) return '—';
|
|
248
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
249
|
+
let v = n;
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
252
|
+
v /= 1024;
|
|
253
|
+
i += 1;
|
|
254
|
+
}
|
|
255
|
+
const digits = v < 10 && i > 0 ? 1 : 0;
|
|
256
|
+
return `${v.toFixed(digits)} ${units[i]}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const quotaPercent = computed(() => {
|
|
260
|
+
const q = props.quota;
|
|
261
|
+
if (!q || q.unlimited || q.total <= 0) return 0;
|
|
262
|
+
return Math.max(0, Math.min(100, Math.round((q.used / q.total) * 100)));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const quotaText = computed(() => {
|
|
266
|
+
const q = props.quota;
|
|
267
|
+
if (!q) return '';
|
|
268
|
+
return q.unlimited || q.total <= 0
|
|
269
|
+
? t('drive.storage.used_unlimited', { used: formatBytes(q.used) })
|
|
270
|
+
: t('drive.storage.used', { used: formatBytes(q.used), total: formatBytes(q.total) });
|
|
271
|
+
});
|
|
272
|
+
|
|
171
273
|
const toggleLabel = computed(() =>
|
|
172
274
|
props.narrow
|
|
173
275
|
? t('sidenav.close')
|
|
@@ -238,7 +340,35 @@ const toggleLabel = computed(() =>
|
|
|
238
340
|
that appears and disappears makes every row below it jump by 90px each
|
|
239
341
|
time the user opens a view, which reads as the panel reloading. -->
|
|
240
342
|
<div class="fe-sidenav__primary">
|
|
343
|
+
<!-- surucu:d1 — one primary action, the shape #14's mockups draw. The
|
|
344
|
+
two-button block below is what every other profile still renders. -->
|
|
345
|
+
<button
|
|
346
|
+
v-if="newMenu"
|
|
347
|
+
ref="newBtnEl"
|
|
348
|
+
type="button"
|
|
349
|
+
class="fe-sidenav__new"
|
|
350
|
+
aria-haspopup="menu"
|
|
351
|
+
:title="t('drive.new')"
|
|
352
|
+
:aria-label="t('drive.new')"
|
|
353
|
+
data-testid="sidenav-new"
|
|
354
|
+
@click="openNewMenu"
|
|
355
|
+
>
|
|
356
|
+
<svg
|
|
357
|
+
class="fe-ficon"
|
|
358
|
+
viewBox="0 0 24 24"
|
|
359
|
+
fill="none"
|
|
360
|
+
stroke="currentColor"
|
|
361
|
+
stroke-width="2"
|
|
362
|
+
stroke-linecap="round"
|
|
363
|
+
aria-hidden="true"
|
|
364
|
+
focusable="false"
|
|
365
|
+
>
|
|
366
|
+
<path d="M12 5v14M5 12h14" />
|
|
367
|
+
</svg>
|
|
368
|
+
<span v-if="showLabels" class="fe-sidenav__text">{{ t('drive.new') }}</span>
|
|
369
|
+
</button>
|
|
241
370
|
<button
|
|
371
|
+
v-if="!newMenu"
|
|
242
372
|
type="button"
|
|
243
373
|
class="fe-sidenav__upload"
|
|
244
374
|
:disabled="!writable"
|
|
@@ -265,6 +395,7 @@ const toggleLabel = computed(() =>
|
|
|
265
395
|
<span v-if="showLabels" class="fe-sidenav__text">{{ t('toolbar.upload') }}</span>
|
|
266
396
|
</button>
|
|
267
397
|
<button
|
|
398
|
+
v-if="!newMenu"
|
|
268
399
|
type="button"
|
|
269
400
|
class="fe-sidenav__secondary"
|
|
270
401
|
:disabled="!writable"
|
|
@@ -568,5 +699,34 @@ const toggleLabel = computed(() =>
|
|
|
568
699
|
</ul>
|
|
569
700
|
</div>
|
|
570
701
|
</div>
|
|
702
|
+
|
|
703
|
+
<!-- surucu:d1 — the storage line. Under the navigation, above nothing:
|
|
704
|
+
it is the last thing in the panel because it is a status, not a
|
|
705
|
+
destination. Hidden entirely when the server did not answer (an app
|
|
706
|
+
token has no person to have a quota) rather than drawn empty. -->
|
|
707
|
+
<div v-if="quota" class="fe-sidenav__quota" data-testid="sidenav-quota">
|
|
708
|
+
<p v-if="showLabels" class="fe-sidenav__quota-label">{{ t('drive.storage.label') }}</p>
|
|
709
|
+
<div
|
|
710
|
+
class="fe-sidenav__quota-bar"
|
|
711
|
+
role="progressbar"
|
|
712
|
+
:aria-valuenow="quotaPercent"
|
|
713
|
+
aria-valuemin="0"
|
|
714
|
+
aria-valuemax="100"
|
|
715
|
+
:aria-label="quotaText"
|
|
716
|
+
:title="quotaText"
|
|
717
|
+
>
|
|
718
|
+
<span class="fe-sidenav__quota-fill" :style="{ width: quotaPercent + '%' }"></span>
|
|
719
|
+
</div>
|
|
720
|
+
<p v-if="showLabels" class="fe-sidenav__quota-text">{{ quotaText }}</p>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<ContextMenu
|
|
724
|
+
v-if="newMenu"
|
|
725
|
+
ref="newMenuRef"
|
|
726
|
+
:locale="locale"
|
|
727
|
+
:theme="theme"
|
|
728
|
+
:actions="newActions"
|
|
729
|
+
@select="onNewSelect"
|
|
730
|
+
/>
|
|
571
731
|
</nav>
|
|
572
732
|
</template>
|
|
@@ -10,6 +10,7 @@ import { ref, watch, computed } from 'vue';
|
|
|
10
10
|
import { setNodeStarred } from '../lib/star';
|
|
11
11
|
import { useLocale } from '../composables/useLocale';
|
|
12
12
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
13
|
+
import { resolveLocale } from '../locales/resolve';
|
|
13
14
|
|
|
14
15
|
const props = defineProps<{
|
|
15
16
|
starred: boolean;
|
|
@@ -45,7 +46,7 @@ const emit = defineEmits<{
|
|
|
45
46
|
const local = ref(props.starred);
|
|
46
47
|
watch(() => props.starred, (v) => { local.value = v; });
|
|
47
48
|
|
|
48
|
-
const { t } = useLocale(() => props.locale
|
|
49
|
+
const { t } = useLocale(() => resolveLocale(props.locale));
|
|
49
50
|
const label = computed(() => t(local.value ? 'ctx.unstar' : 'ctx.star'));
|
|
50
51
|
|
|
51
52
|
async function toggle() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* TabBar — wiring:d1
|
|
3
|
+
* TabBar — wiring:d1 the tab strip.
|
|
4
4
|
*
|
|
5
5
|
* Thin strip that sits ABOVE the toolbar. The host renders it only when
|
|
6
6
|
* 2+ tabs exist (embed pixel-parity: a single tab shows nothing at all).
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Interactions:
|
|
10
10
|
* click tab → select
|
|
11
|
-
*
|
|
11
|
+
* ×/middle-click → close
|
|
12
12
|
* + → new tab (clone of the current location)
|
|
13
13
|
* drag → reorder (HTML5 DnD; live-swap while dragging)
|
|
14
14
|
* ⫿ (split) → toggle the active tab's secondary pane
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* toolbar chips + sample rows) painted with that theme's tokens for the
|
|
7
7
|
* CURRENTLY resolved light/dark mode — so the card shows exactly what
|
|
8
8
|
* you'd get. Click applies instantly (live preview behind the modal) and
|
|
9
|
-
* persists; the selected card carries a ✓. "
|
|
9
|
+
* persists; the selected card carries a ✓. "Reset to default" resets.
|
|
10
10
|
*
|
|
11
11
|
* Presentational only — selection state + persistence live in
|
|
12
12
|
* lib/themes.ts (shared across instances); the parent forwards `select`.
|
|
@@ -30,6 +30,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
|
30
30
|
import type { ApiToken } from '../types/Tokens';
|
|
31
31
|
import { useLocale } from '../composables/useLocale';
|
|
32
32
|
import { useTokens } from '../composables/useTokens';
|
|
33
|
+
import { resolveLocale } from '../locales/resolve';
|
|
33
34
|
|
|
34
35
|
const props = defineProps<{
|
|
35
36
|
config: ExplorerConfig;
|
|
@@ -50,7 +51,7 @@ const emit = defineEmits<{
|
|
|
50
51
|
(e: 'active', v: { hasToken: boolean }): void;
|
|
51
52
|
}>();
|
|
52
53
|
|
|
53
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
54
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
54
55
|
const { t } = useLocale(locale);
|
|
55
56
|
|
|
56
57
|
const { tokens, loading, error, canMint, revealed, load, create, remove, dismiss } = useTokens(
|