@brftech/filex-core 0.1.56 → 0.1.60

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.
Files changed (82) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +108 -108
  3. package/dist/{ArchiveViewer-CJdaw5x3.js → ArchiveViewer-CbUUEREI.js} +20 -20
  4. package/dist/ArchiveViewer-CbUUEREI.js.map +1 -0
  5. package/dist/{CsvViewer-QYx_tePQ.js → CsvViewer-72rDsQJX.js} +3 -3
  6. package/dist/CsvViewer-72rDsQJX.js.map +1 -0
  7. package/dist/{DrawioViewer-GWt0TyB7.js → DrawioViewer-DSyMwGvg.js} +7 -7
  8. package/dist/DrawioViewer-DSyMwGvg.js.map +1 -0
  9. package/dist/{EpubViewer-Je0ywklQ.js → EpubViewer-DvDa17d7.js} +3 -3
  10. package/dist/EpubViewer-DvDa17d7.js.map +1 -0
  11. package/dist/{IpynbViewer-TsB4gqP-.js → IpynbViewer-DibIkdsz.js} +3 -3
  12. package/dist/IpynbViewer-DibIkdsz.js.map +1 -0
  13. package/dist/{MermaidViewer-CXfeDS-7.js → MermaidViewer-BoFLtGvl.js} +3 -3
  14. package/dist/MermaidViewer-BoFLtGvl.js.map +1 -0
  15. package/dist/{PsdViewer-VtPpxKVe.js → PsdViewer-K2iZkwax.js} +3 -3
  16. package/dist/PsdViewer-K2iZkwax.js.map +1 -0
  17. package/dist/{TiffViewer-OINn9bbb.js → TiffViewer-JzDzbqpO.js} +3 -3
  18. package/dist/TiffViewer-JzDzbqpO.js.map +1 -0
  19. package/dist/{Viewer3D-xvvyiWHd.js → Viewer3D-WEMYCP8l.js} +7 -7
  20. package/dist/Viewer3D-WEMYCP8l.js.map +1 -0
  21. package/dist/filex-core.js +1 -1
  22. package/dist/filex-core.umd.cjs +4 -4
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/{index-CmSYzt6L.js → index-B5YT-m5Q.js} +209 -209
  25. package/dist/index-B5YT-m5Q.js.map +1 -0
  26. package/dist/style.css +1 -1
  27. package/dist/useViewerFetch-czqbd2Lj.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/Breadcrumb.vue +282 -282
  30. package/src/components/ContextMenu.vue +152 -152
  31. package/src/components/GridView.vue +166 -166
  32. package/src/components/ListView.vue +212 -212
  33. package/src/components/PendingOpsTray.vue +91 -91
  34. package/src/components/RecentlyOpened.vue +155 -155
  35. package/src/components/StarButton.vue +101 -101
  36. package/src/components/TagPicker.vue +184 -184
  37. package/src/components/Toolbar.vue +220 -220
  38. package/src/components/UploadProgress.vue +78 -78
  39. package/src/composables/useFileApi.ts +692 -692
  40. package/src/composables/useKeyboardShortcuts.ts +144 -144
  41. package/src/composables/useLocale.ts +60 -60
  42. package/src/composables/useMonacoLoader.ts +98 -98
  43. package/src/composables/usePendingOps.ts +200 -200
  44. package/src/composables/useSelection.ts +81 -81
  45. package/src/composables/useUploadChunked.ts +250 -250
  46. package/src/composables/useViewerFetch.ts +58 -58
  47. package/src/index.ts +80 -80
  48. package/src/locales/en.ts +122 -122
  49. package/src/locales/index.ts +12 -12
  50. package/src/locales/tr.ts +122 -122
  51. package/src/modals/DeleteConfirmModal.vue +32 -32
  52. package/src/modals/Modal.vue +104 -104
  53. package/src/modals/NewFolderModal.vue +64 -64
  54. package/src/modals/PermissionsModal.vue +815 -815
  55. package/src/modals/PreviewModal.vue +1070 -1070
  56. package/src/modals/RenameModal.vue +58 -58
  57. package/src/modals/ShareModal.vue +135 -135
  58. package/src/styles/base.css +1224 -1224
  59. package/src/styles/variables.css +90 -90
  60. package/src/types/FileNode.ts +136 -136
  61. package/src/types/index.ts +27 -27
  62. package/src/types/peers.d.ts +45 -45
  63. package/src/viewers/ArchiveViewer.vue +186 -186
  64. package/src/viewers/CsvViewer.vue +319 -319
  65. package/src/viewers/DrawioViewer.vue +243 -243
  66. package/src/viewers/EpubViewer.vue +307 -307
  67. package/src/viewers/IpynbViewer.vue +365 -365
  68. package/src/viewers/MermaidViewer.vue +259 -259
  69. package/src/viewers/PdfViewer.vue +562 -562
  70. package/src/viewers/PsdViewer.vue +264 -264
  71. package/src/viewers/TiffViewer.vue +273 -273
  72. package/src/viewers/Viewer3D.vue +120 -120
  73. package/dist/ArchiveViewer-CJdaw5x3.js.map +0 -1
  74. package/dist/CsvViewer-QYx_tePQ.js.map +0 -1
  75. package/dist/DrawioViewer-GWt0TyB7.js.map +0 -1
  76. package/dist/EpubViewer-Je0ywklQ.js.map +0 -1
  77. package/dist/IpynbViewer-TsB4gqP-.js.map +0 -1
  78. package/dist/MermaidViewer-CXfeDS-7.js.map +0 -1
  79. package/dist/PsdViewer-VtPpxKVe.js.map +0 -1
  80. package/dist/TiffViewer-OINn9bbb.js.map +0 -1
  81. package/dist/Viewer3D-xvvyiWHd.js.map +0 -1
  82. package/dist/index-CmSYzt6L.js.map +0 -1
@@ -1,152 +1,152 @@
1
- <script setup lang="ts">
2
- /**
3
- * ContextMenu — Teleport-based menu.
4
- *
5
- * Teleported under <body> so no parent (Ionic <ion-page>, Filament's
6
- * transformed flex containers, panel sidebars, …) can shift the
7
- * `position: fixed` backdrop off-window.
8
- */
9
- import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
10
- import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
11
- import type { FileNode } from '../types/FileNode';
12
- import { useLocale } from '../composables/useLocale';
13
-
14
- export interface ContextAction {
15
- key: string;
16
- label: string;
17
- icon?: string;
18
- danger?: boolean;
19
- disabled?: boolean;
20
- hidden?: boolean;
21
- divider?: boolean;
22
- }
23
-
24
- const props = defineProps<{
25
- locale: LocaleCode;
26
- actions: ContextAction[];
27
- /**
28
- * Resolved theme. Teleported into <body> we lose the `.fe` parent
29
- * that scopes CSS vars, so we tag the backdrop with the current
30
- * theme so variables.css can re-apply dark overrides outside the
31
- * explorer tree.
32
- */
33
- theme?: ThemeMode;
34
- }>();
35
-
36
- const emit = defineEmits<{
37
- (e: 'select', action: ContextAction, target: FileNode[]): void;
38
- }>();
39
-
40
- void useLocale(() => props.locale); // eager-instantiate t/lookup so future templates can reuse
41
-
42
- const open = ref(false);
43
- const x = ref(0);
44
- const y = ref(0);
45
- const targetNodes = ref<FileNode[]>([]);
46
- const menuEl = ref<HTMLElement | null>(null);
47
-
48
- async function show(ev: { clientX: number; clientY: number }, nodes: FileNode[]) {
49
- open.value = true;
50
- x.value = ev.clientX;
51
- y.value = ev.clientY;
52
- targetNodes.value = nodes;
53
- // After the menu renders, clamp it inside the viewport so a click near the
54
- // bottom/right edge doesn't push the menu off-screen (it opens down-right
55
- // from the cursor by default).
56
- await nextTick();
57
- clampToViewport();
58
- }
59
-
60
- function clampToViewport() {
61
- const el = menuEl.value;
62
- if (!el || typeof window === 'undefined') return;
63
- const rect = el.getBoundingClientRect();
64
- const margin = 8;
65
- const vw = window.innerWidth;
66
- const vh = window.innerHeight;
67
- if (y.value + rect.height > vh - margin) {
68
- y.value = Math.max(margin, vh - rect.height - margin);
69
- }
70
- if (x.value + rect.width > vw - margin) {
71
- x.value = Math.max(margin, vw - rect.width - margin);
72
- }
73
- }
74
-
75
- function hide() {
76
- open.value = false;
77
- }
78
-
79
- function pick(a: ContextAction) {
80
- if (a.disabled) return;
81
- emit('select', a, targetNodes.value);
82
- hide();
83
- }
84
-
85
- function onKey(e: KeyboardEvent) {
86
- if (e.key === 'Escape') hide();
87
- }
88
-
89
- const visibleActions = computed(() => props.actions.filter((a) => !a.hidden));
90
-
91
- const prefersDark = ref(false);
92
- let mq: MediaQueryList | undefined;
93
- function syncPrefersDark(e?: MediaQueryListEvent | MediaQueryList) {
94
- prefersDark.value = !!(e && 'matches' in e && e.matches);
95
- }
96
- onMounted(() => {
97
- if (typeof window === 'undefined') return;
98
- mq = window.matchMedia('(prefers-color-scheme: dark)');
99
- syncPrefersDark(mq);
100
- mq.addEventListener?.('change', syncPrefersDark);
101
- });
102
- onBeforeUnmount(() => {
103
- mq?.removeEventListener?.('change', syncPrefersDark);
104
- });
105
-
106
- const themeClass = computed(() => {
107
- const t = props.theme || 'auto';
108
- return `fe-ctx-backdrop--theme-${t}`;
109
- });
110
-
111
- defineExpose({ show, hide });
112
- </script>
113
-
114
- <template>
115
- <Teleport to="body">
116
- <transition name="fe-ctx">
117
- <div
118
- v-if="open"
119
- class="fe-ctx-backdrop"
120
- :class="themeClass"
121
- :data-prefers-dark="prefersDark ? '1' : '0'"
122
- @click="hide"
123
- @contextmenu.prevent="hide"
124
- @keydown="onKey"
125
- >
126
- <div
127
- ref="menuEl"
128
- class="fe-ctx"
129
- role="menu"
130
- :style="{ top: y + 'px', left: x + 'px' }"
131
- @click.stop
132
- >
133
- <template v-for="(a, i) in visibleActions" :key="a.key || i">
134
- <div v-if="a.divider" class="fe-ctx__sep" />
135
- <button
136
- v-else
137
- type="button"
138
- class="fe-ctx__item"
139
- :class="{ 'is-danger': a.danger, 'is-disabled': a.disabled }"
140
- :disabled="a.disabled"
141
- role="menuitem"
142
- @click="pick(a)"
143
- >
144
- <span v-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
145
- <span class="fe-ctx__label">{{ a.label }}</span>
146
- </button>
147
- </template>
148
- </div>
149
- </div>
150
- </transition>
151
- </Teleport>
152
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * ContextMenu — Teleport-based menu.
4
+ *
5
+ * Teleported under <body> so no parent (Ionic <ion-page>, Filament's
6
+ * transformed flex containers, panel sidebars, …) can shift the
7
+ * `position: fixed` backdrop off-window.
8
+ */
9
+ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
10
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
11
+ import type { FileNode } from '../types/FileNode';
12
+ import { useLocale } from '../composables/useLocale';
13
+
14
+ export interface ContextAction {
15
+ key: string;
16
+ label: string;
17
+ icon?: string;
18
+ danger?: boolean;
19
+ disabled?: boolean;
20
+ hidden?: boolean;
21
+ divider?: boolean;
22
+ }
23
+
24
+ const props = defineProps<{
25
+ locale: LocaleCode;
26
+ actions: ContextAction[];
27
+ /**
28
+ * Resolved theme. Teleported into <body> we lose the `.fe` parent
29
+ * that scopes CSS vars, so we tag the backdrop with the current
30
+ * theme so variables.css can re-apply dark overrides outside the
31
+ * explorer tree.
32
+ */
33
+ theme?: ThemeMode;
34
+ }>();
35
+
36
+ const emit = defineEmits<{
37
+ (e: 'select', action: ContextAction, target: FileNode[]): void;
38
+ }>();
39
+
40
+ void useLocale(() => props.locale); // eager-instantiate t/lookup so future templates can reuse
41
+
42
+ const open = ref(false);
43
+ const x = ref(0);
44
+ const y = ref(0);
45
+ const targetNodes = ref<FileNode[]>([]);
46
+ const menuEl = ref<HTMLElement | null>(null);
47
+
48
+ async function show(ev: { clientX: number; clientY: number }, nodes: FileNode[]) {
49
+ open.value = true;
50
+ x.value = ev.clientX;
51
+ y.value = ev.clientY;
52
+ targetNodes.value = nodes;
53
+ // After the menu renders, clamp it inside the viewport so a click near the
54
+ // bottom/right edge doesn't push the menu off-screen (it opens down-right
55
+ // from the cursor by default).
56
+ await nextTick();
57
+ clampToViewport();
58
+ }
59
+
60
+ function clampToViewport() {
61
+ const el = menuEl.value;
62
+ if (!el || typeof window === 'undefined') return;
63
+ const rect = el.getBoundingClientRect();
64
+ const margin = 8;
65
+ const vw = window.innerWidth;
66
+ const vh = window.innerHeight;
67
+ if (y.value + rect.height > vh - margin) {
68
+ y.value = Math.max(margin, vh - rect.height - margin);
69
+ }
70
+ if (x.value + rect.width > vw - margin) {
71
+ x.value = Math.max(margin, vw - rect.width - margin);
72
+ }
73
+ }
74
+
75
+ function hide() {
76
+ open.value = false;
77
+ }
78
+
79
+ function pick(a: ContextAction) {
80
+ if (a.disabled) return;
81
+ emit('select', a, targetNodes.value);
82
+ hide();
83
+ }
84
+
85
+ function onKey(e: KeyboardEvent) {
86
+ if (e.key === 'Escape') hide();
87
+ }
88
+
89
+ const visibleActions = computed(() => props.actions.filter((a) => !a.hidden));
90
+
91
+ const prefersDark = ref(false);
92
+ let mq: MediaQueryList | undefined;
93
+ function syncPrefersDark(e?: MediaQueryListEvent | MediaQueryList) {
94
+ prefersDark.value = !!(e && 'matches' in e && e.matches);
95
+ }
96
+ onMounted(() => {
97
+ if (typeof window === 'undefined') return;
98
+ mq = window.matchMedia('(prefers-color-scheme: dark)');
99
+ syncPrefersDark(mq);
100
+ mq.addEventListener?.('change', syncPrefersDark);
101
+ });
102
+ onBeforeUnmount(() => {
103
+ mq?.removeEventListener?.('change', syncPrefersDark);
104
+ });
105
+
106
+ const themeClass = computed(() => {
107
+ const t = props.theme || 'auto';
108
+ return `fe-ctx-backdrop--theme-${t}`;
109
+ });
110
+
111
+ defineExpose({ show, hide });
112
+ </script>
113
+
114
+ <template>
115
+ <Teleport to="body">
116
+ <transition name="fe-ctx">
117
+ <div
118
+ v-if="open"
119
+ class="fe-ctx-backdrop"
120
+ :class="themeClass"
121
+ :data-prefers-dark="prefersDark ? '1' : '0'"
122
+ @click="hide"
123
+ @contextmenu.prevent="hide"
124
+ @keydown="onKey"
125
+ >
126
+ <div
127
+ ref="menuEl"
128
+ class="fe-ctx"
129
+ role="menu"
130
+ :style="{ top: y + 'px', left: x + 'px' }"
131
+ @click.stop
132
+ >
133
+ <template v-for="(a, i) in visibleActions" :key="a.key || i">
134
+ <div v-if="a.divider" class="fe-ctx__sep" />
135
+ <button
136
+ v-else
137
+ type="button"
138
+ class="fe-ctx__item"
139
+ :class="{ 'is-danger': a.danger, 'is-disabled': a.disabled }"
140
+ :disabled="a.disabled"
141
+ role="menuitem"
142
+ @click="pick(a)"
143
+ >
144
+ <span v-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
145
+ <span class="fe-ctx__label">{{ a.label }}</span>
146
+ </button>
147
+ </template>
148
+ </div>
149
+ </div>
150
+ </transition>
151
+ </Teleport>
152
+ </template>
@@ -1,166 +1,166 @@
1
- <script setup lang="ts">
2
- /**
3
- * GridView — card grid. Thumbnails preferred, fall back to icon.
4
- */
5
- import type { FileNode } from '../types/FileNode';
6
- import type { LocaleCode } from '../types/ExplorerConfig';
7
- import { useLocale } from '../composables/useLocale';
8
-
9
- const props = defineProps<{
10
- files: FileNode[];
11
- selected: Set<string>;
12
- clipped?: Set<string>;
13
- showParentPath?: boolean;
14
- locale: LocaleCode;
15
- loading?: boolean;
16
- }>();
17
-
18
- const emit = defineEmits<{
19
- (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
20
- (e: 'dbl-card', node: FileNode): void;
21
- (e: 'context-card', node: FileNode, ev: MouseEvent): void;
22
- (e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
23
- (e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
24
- }>();
25
-
26
- const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
27
-
28
- function isSelected(n: FileNode): boolean {
29
- return props.selected.has(n.path);
30
- }
31
-
32
- function onClick(n: FileNode, ev: MouseEvent) {
33
- emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey });
34
- }
35
-
36
- function onDbl(n: FileNode) {
37
- emit('dbl-card', n);
38
- }
39
-
40
- function onCtx(n: FileNode, ev: MouseEvent) {
41
- ev.preventDefault();
42
- ev.stopPropagation();
43
- emit('context-card', n, ev);
44
- }
45
-
46
- function onItemDragStart(n: FileNode, ev: DragEvent) {
47
- emit('item-drag-start', n, ev);
48
- }
49
-
50
- function onItemDragOver(n: FileNode, ev: DragEvent) {
51
- if (n.type !== 'dir') return;
52
- if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
53
- ev.preventDefault();
54
- ev.stopPropagation();
55
- if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
56
- }
57
-
58
- function onItemDrop(n: FileNode, ev: DragEvent) {
59
- if (n.type !== 'dir') return;
60
- if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
61
- ev.preventDefault();
62
- ev.stopPropagation();
63
- emit('item-drop-into', n, ev);
64
- }
65
-
66
- let pressTimer: ReturnType<typeof setTimeout> | undefined;
67
- let pressTarget: FileNode | null = null;
68
- function onTouchStart(n: FileNode, ev: TouchEvent) {
69
- pressTarget = n;
70
- if (pressTimer) clearTimeout(pressTimer);
71
- pressTimer = setTimeout(() => {
72
- if (pressTarget) {
73
- const t0 = ev.touches[0];
74
- emit('context-card', pressTarget, {
75
- clientX: t0.clientX,
76
- clientY: t0.clientY,
77
- preventDefault: () => {},
78
- } as unknown as MouseEvent);
79
- }
80
- }, 500);
81
- }
82
- function cancelPress() {
83
- if (pressTimer) clearTimeout(pressTimer);
84
- pressTarget = null;
85
- }
86
-
87
- function parentDir(path: string): string {
88
- const stripped = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
89
- const idx = stripped.lastIndexOf('/');
90
- if (idx === -1) return '';
91
- return stripped.slice(0, idx);
92
- }
93
-
94
- function iconFor(n: FileNode): string {
95
- if (n.mime_type === 'inode/storage') return '💾';
96
- if (n.type === 'dir') return '📁';
97
- const e = (n.extension || '').toLowerCase();
98
- if (['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'svg'].includes(e)) return '🖼';
99
- if (['mp4', 'webm', 'mov', 'mkv'].includes(e)) return '🎞';
100
- if (['mp3', 'wav', 'flac', 'ogg'].includes(e)) return '🎵';
101
- if (e === 'pdf') return '📕';
102
- if (['doc', 'docx', 'odt'].includes(e)) return '📄';
103
- if (['xls', 'xlsx', 'csv'].includes(e)) return '📊';
104
- if (['ppt', 'pptx'].includes(e)) return '📽';
105
- if (['zip', 'tar', 'gz', '7z'].includes(e)) return '🗜';
106
- return '📎';
107
- }
108
- </script>
109
-
110
- <template>
111
- <div class="fe-grid" :class="{ 'is-loading': loading }">
112
- <div
113
- v-for="n in files"
114
- :key="n.path"
115
- class="fe-grid__card"
116
- :class="{
117
- 'is-selected': isSelected(n),
118
- 'is-dir': n.type === 'dir',
119
- 'is-trash': n.trashed,
120
- 'is-clipped': clipped?.has(n.path),
121
- }"
122
- tabindex="0"
123
- draggable="true"
124
- @click="onClick(n, $event)"
125
- @dblclick="onDbl(n)"
126
- @contextmenu="onCtx(n, $event)"
127
- @dragstart="onItemDragStart(n, $event)"
128
- @dragover="onItemDragOver(n, $event)"
129
- @drop="onItemDrop(n, $event)"
130
- @touchstart.passive="onTouchStart(n, $event)"
131
- @touchend="cancelPress"
132
- @touchmove="cancelPress"
133
- >
134
- <div class="fe-grid__thumb">
135
- <!--
136
- draggable="false" — HTML5 image drag dataTransfer adds 'Files'
137
- MIME, which trips the parent's upload handler; without this
138
- the user dragging an item with a thumbnail re-uploads it.
139
- Parent card stays draggable=true so internal move works.
140
- -->
141
- <img
142
- v-if="n.thumb_url"
143
- :src="n.thumb_url"
144
- :alt="n.basename"
145
- loading="lazy"
146
- draggable="false"
147
- />
148
- <span v-else class="fe-grid__icon">{{ iconFor(n) }}</span>
149
- </div>
150
- <div class="fe-grid__label" :title="n.basename">
151
- {{ nodeDisplayName(n) }}
152
- </div>
153
- <div
154
- v-if="showParentPath"
155
- class="fe-grid__parent"
156
- :title="parentDir(n.path)"
157
- >{{ parentDir(n.path) || '—' }}</div>
158
- <div class="fe-grid__meta">
159
- {{ n.type === 'dir' ? '—' : formatSize(n.size) }}
160
- </div>
161
- </div>
162
- <div v-if="!loading && files.length === 0" class="fe-grid__empty">
163
- {{ t('empty.folder') }}
164
- </div>
165
- </div>
166
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * GridView — card grid. Thumbnails preferred, fall back to icon.
4
+ */
5
+ import type { FileNode } from '../types/FileNode';
6
+ import type { LocaleCode } from '../types/ExplorerConfig';
7
+ import { useLocale } from '../composables/useLocale';
8
+
9
+ const props = defineProps<{
10
+ files: FileNode[];
11
+ selected: Set<string>;
12
+ clipped?: Set<string>;
13
+ showParentPath?: boolean;
14
+ locale: LocaleCode;
15
+ loading?: boolean;
16
+ }>();
17
+
18
+ const emit = defineEmits<{
19
+ (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
20
+ (e: 'dbl-card', node: FileNode): void;
21
+ (e: 'context-card', node: FileNode, ev: MouseEvent): void;
22
+ (e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
23
+ (e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
24
+ }>();
25
+
26
+ const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
27
+
28
+ function isSelected(n: FileNode): boolean {
29
+ return props.selected.has(n.path);
30
+ }
31
+
32
+ function onClick(n: FileNode, ev: MouseEvent) {
33
+ emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey });
34
+ }
35
+
36
+ function onDbl(n: FileNode) {
37
+ emit('dbl-card', n);
38
+ }
39
+
40
+ function onCtx(n: FileNode, ev: MouseEvent) {
41
+ ev.preventDefault();
42
+ ev.stopPropagation();
43
+ emit('context-card', n, ev);
44
+ }
45
+
46
+ function onItemDragStart(n: FileNode, ev: DragEvent) {
47
+ emit('item-drag-start', n, ev);
48
+ }
49
+
50
+ function onItemDragOver(n: FileNode, ev: DragEvent) {
51
+ if (n.type !== 'dir') return;
52
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
53
+ ev.preventDefault();
54
+ ev.stopPropagation();
55
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
56
+ }
57
+
58
+ function onItemDrop(n: FileNode, ev: DragEvent) {
59
+ if (n.type !== 'dir') return;
60
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
61
+ ev.preventDefault();
62
+ ev.stopPropagation();
63
+ emit('item-drop-into', n, ev);
64
+ }
65
+
66
+ let pressTimer: ReturnType<typeof setTimeout> | undefined;
67
+ let pressTarget: FileNode | null = null;
68
+ function onTouchStart(n: FileNode, ev: TouchEvent) {
69
+ pressTarget = n;
70
+ if (pressTimer) clearTimeout(pressTimer);
71
+ pressTimer = setTimeout(() => {
72
+ if (pressTarget) {
73
+ const t0 = ev.touches[0];
74
+ emit('context-card', pressTarget, {
75
+ clientX: t0.clientX,
76
+ clientY: t0.clientY,
77
+ preventDefault: () => {},
78
+ } as unknown as MouseEvent);
79
+ }
80
+ }, 500);
81
+ }
82
+ function cancelPress() {
83
+ if (pressTimer) clearTimeout(pressTimer);
84
+ pressTarget = null;
85
+ }
86
+
87
+ function parentDir(path: string): string {
88
+ const stripped = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
89
+ const idx = stripped.lastIndexOf('/');
90
+ if (idx === -1) return '';
91
+ return stripped.slice(0, idx);
92
+ }
93
+
94
+ function iconFor(n: FileNode): string {
95
+ if (n.mime_type === 'inode/storage') return '💾';
96
+ if (n.type === 'dir') return '📁';
97
+ const e = (n.extension || '').toLowerCase();
98
+ if (['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'svg'].includes(e)) return '🖼';
99
+ if (['mp4', 'webm', 'mov', 'mkv'].includes(e)) return '🎞';
100
+ if (['mp3', 'wav', 'flac', 'ogg'].includes(e)) return '🎵';
101
+ if (e === 'pdf') return '📕';
102
+ if (['doc', 'docx', 'odt'].includes(e)) return '📄';
103
+ if (['xls', 'xlsx', 'csv'].includes(e)) return '📊';
104
+ if (['ppt', 'pptx'].includes(e)) return '📽';
105
+ if (['zip', 'tar', 'gz', '7z'].includes(e)) return '🗜';
106
+ return '📎';
107
+ }
108
+ </script>
109
+
110
+ <template>
111
+ <div class="fe-grid" :class="{ 'is-loading': loading }">
112
+ <div
113
+ v-for="n in files"
114
+ :key="n.path"
115
+ class="fe-grid__card"
116
+ :class="{
117
+ 'is-selected': isSelected(n),
118
+ 'is-dir': n.type === 'dir',
119
+ 'is-trash': n.trashed,
120
+ 'is-clipped': clipped?.has(n.path),
121
+ }"
122
+ tabindex="0"
123
+ draggable="true"
124
+ @click="onClick(n, $event)"
125
+ @dblclick="onDbl(n)"
126
+ @contextmenu="onCtx(n, $event)"
127
+ @dragstart="onItemDragStart(n, $event)"
128
+ @dragover="onItemDragOver(n, $event)"
129
+ @drop="onItemDrop(n, $event)"
130
+ @touchstart.passive="onTouchStart(n, $event)"
131
+ @touchend="cancelPress"
132
+ @touchmove="cancelPress"
133
+ >
134
+ <div class="fe-grid__thumb">
135
+ <!--
136
+ draggable="false" — HTML5 image drag dataTransfer adds 'Files'
137
+ MIME, which trips the parent's upload handler; without this
138
+ the user dragging an item with a thumbnail re-uploads it.
139
+ Parent card stays draggable=true so internal move works.
140
+ -->
141
+ <img
142
+ v-if="n.thumb_url"
143
+ :src="n.thumb_url"
144
+ :alt="n.basename"
145
+ loading="lazy"
146
+ draggable="false"
147
+ />
148
+ <span v-else class="fe-grid__icon">{{ iconFor(n) }}</span>
149
+ </div>
150
+ <div class="fe-grid__label" :title="n.basename">
151
+ {{ nodeDisplayName(n) }}
152
+ </div>
153
+ <div
154
+ v-if="showParentPath"
155
+ class="fe-grid__parent"
156
+ :title="parentDir(n.path)"
157
+ >{{ parentDir(n.path) || '—' }}</div>
158
+ <div class="fe-grid__meta">
159
+ {{ formatSize(n.size) }}
160
+ </div>
161
+ </div>
162
+ <div v-if="!loading && files.length === 0" class="fe-grid__empty">
163
+ {{ t('empty.folder') }}
164
+ </div>
165
+ </div>
166
+ </template>