@brftech/filex-core 0.1.56 → 0.1.61
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/LICENSE +21 -21
- package/README.md +108 -108
- package/dist/{ArchiveViewer-CJdaw5x3.js → ArchiveViewer-CbUUEREI.js} +20 -20
- package/dist/ArchiveViewer-CbUUEREI.js.map +1 -0
- package/dist/{CsvViewer-QYx_tePQ.js → CsvViewer-72rDsQJX.js} +3 -3
- package/dist/CsvViewer-72rDsQJX.js.map +1 -0
- package/dist/{DrawioViewer-GWt0TyB7.js → DrawioViewer-DSyMwGvg.js} +7 -7
- package/dist/DrawioViewer-DSyMwGvg.js.map +1 -0
- package/dist/{EpubViewer-Je0ywklQ.js → EpubViewer-DvDa17d7.js} +3 -3
- package/dist/EpubViewer-DvDa17d7.js.map +1 -0
- package/dist/{IpynbViewer-TsB4gqP-.js → IpynbViewer-DibIkdsz.js} +3 -3
- package/dist/IpynbViewer-DibIkdsz.js.map +1 -0
- package/dist/{MermaidViewer-CXfeDS-7.js → MermaidViewer-BoFLtGvl.js} +3 -3
- package/dist/MermaidViewer-BoFLtGvl.js.map +1 -0
- package/dist/{PsdViewer-VtPpxKVe.js → PsdViewer-K2iZkwax.js} +3 -3
- package/dist/PsdViewer-K2iZkwax.js.map +1 -0
- package/dist/{TiffViewer-OINn9bbb.js → TiffViewer-JzDzbqpO.js} +3 -3
- package/dist/TiffViewer-JzDzbqpO.js.map +1 -0
- package/dist/{Viewer3D-xvvyiWHd.js → Viewer3D-WEMYCP8l.js} +7 -7
- package/dist/Viewer3D-WEMYCP8l.js.map +1 -0
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +4 -4
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-CmSYzt6L.js → index-B5YT-m5Q.js} +209 -209
- package/dist/index-B5YT-m5Q.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumb.vue +282 -282
- package/src/components/ContextMenu.vue +152 -152
- package/src/components/GridView.vue +166 -166
- package/src/components/ListView.vue +212 -212
- package/src/components/PendingOpsTray.vue +91 -91
- package/src/components/RecentlyOpened.vue +155 -155
- package/src/components/StarButton.vue +101 -101
- package/src/components/TagPicker.vue +184 -184
- package/src/components/Toolbar.vue +220 -220
- package/src/components/UploadProgress.vue +78 -78
- package/src/composables/useFileApi.ts +692 -692
- package/src/composables/useKeyboardShortcuts.ts +144 -144
- package/src/composables/useLocale.ts +60 -60
- package/src/composables/useMonacoLoader.ts +98 -98
- package/src/composables/usePendingOps.ts +200 -200
- package/src/composables/useSelection.ts +81 -81
- package/src/composables/useUploadChunked.ts +250 -250
- package/src/composables/useViewerFetch.ts +58 -58
- package/src/index.ts +80 -80
- package/src/locales/en.ts +122 -122
- package/src/locales/index.ts +12 -12
- package/src/locales/tr.ts +122 -122
- package/src/modals/DeleteConfirmModal.vue +32 -32
- package/src/modals/Modal.vue +104 -104
- package/src/modals/NewFolderModal.vue +64 -64
- package/src/modals/PermissionsModal.vue +815 -815
- package/src/modals/PreviewModal.vue +1070 -1070
- package/src/modals/RenameModal.vue +58 -58
- package/src/modals/ShareModal.vue +135 -135
- package/src/styles/base.css +1224 -1224
- package/src/styles/variables.css +90 -90
- package/src/types/FileNode.ts +136 -136
- package/src/types/index.ts +27 -27
- package/src/types/peers.d.ts +45 -45
- package/src/viewers/ArchiveViewer.vue +186 -186
- package/src/viewers/CsvViewer.vue +319 -319
- package/src/viewers/DrawioViewer.vue +243 -243
- package/src/viewers/EpubViewer.vue +307 -307
- package/src/viewers/IpynbViewer.vue +365 -365
- package/src/viewers/MermaidViewer.vue +259 -259
- package/src/viewers/PdfViewer.vue +562 -562
- package/src/viewers/PsdViewer.vue +264 -264
- package/src/viewers/TiffViewer.vue +273 -273
- package/src/viewers/Viewer3D.vue +120 -120
- package/dist/ArchiveViewer-CJdaw5x3.js.map +0 -1
- package/dist/CsvViewer-QYx_tePQ.js.map +0 -1
- package/dist/DrawioViewer-GWt0TyB7.js.map +0 -1
- package/dist/EpubViewer-Je0ywklQ.js.map +0 -1
- package/dist/IpynbViewer-TsB4gqP-.js.map +0 -1
- package/dist/MermaidViewer-CXfeDS-7.js.map +0 -1
- package/dist/PsdViewer-VtPpxKVe.js.map +0 -1
- package/dist/TiffViewer-OINn9bbb.js.map +0 -1
- package/dist/Viewer3D-xvvyiWHd.js.map +0 -1
- 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
|
-
{{
|
|
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>
|