@brftech/filex-core 0.39.1 → 0.41.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 +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +43 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* GridView — card grid.
|
|
3
|
+
* GridView — card grid.
|
|
4
|
+
*
|
|
5
|
+
* gorunum:v1 — the card has an anatomy now, and it is not the same anatomy
|
|
6
|
+
* for a folder and for a file:
|
|
7
|
+
*
|
|
8
|
+
* folder 186×56 glyph · name over "Folder" · ⋮
|
|
9
|
+
* file 186×166 184×108 preview · 56px footer (24px type tile · name
|
|
10
|
+
* over "1.7 MB • Sep 9, 2026" · ⋮)
|
|
11
|
+
*
|
|
12
|
+
* A folder has no preview to show and no size or date worth printing, so a
|
|
13
|
+
* square thumbnail box above its name was 108px of nothing on the most common
|
|
14
|
+
* row in any listing. The two shapes share one footer row — the folder card
|
|
15
|
+
* IS that row — so there is a single piece of markup to change when the row
|
|
16
|
+
* changes.
|
|
4
17
|
*/
|
|
5
|
-
import { computed, ref } from 'vue'; /* wiring:c4 */
|
|
18
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue'; /* wiring:c4 */
|
|
6
19
|
import type { FileNode } from '../types/FileNode';
|
|
7
20
|
import { hasInternalDrag } from '../lib/dragOut';
|
|
8
21
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
9
22
|
import { useLocale } from '../composables/useLocale';
|
|
10
|
-
import {
|
|
23
|
+
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
24
|
+
import {
|
|
25
|
+
createFilePreviews,
|
|
26
|
+
drawsAsPage,
|
|
27
|
+
drawsAsVideo,
|
|
28
|
+
type FilePreview,
|
|
29
|
+
} from '../lib/filePreview'; /* gorunum:v1-preview */
|
|
30
|
+
import { byFoldersFirst, parentDirOf } from '../lib/listing'; /* gorunum:v1 */
|
|
31
|
+
import {
|
|
32
|
+
groupByDate,
|
|
33
|
+
groupingActive,
|
|
34
|
+
} from '../lib/dateGroups'; /* gruplama */
|
|
35
|
+
import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama */
|
|
11
36
|
import StarButton from './StarButton.vue';
|
|
12
37
|
import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
|
|
13
38
|
import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
|
|
@@ -49,17 +74,39 @@ const props = defineProps<{
|
|
|
49
74
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
50
75
|
authCredentials?: RequestCredentials;
|
|
51
76
|
/**
|
|
52
|
-
* surucu:d1 — draw "Folders" and "Files" as labelled sections
|
|
53
|
-
* (`uiProfile: 'drive'`). Absent/false renders the flat grid unchanged, down
|
|
54
|
-
* to the DOM: the two headings are the only extra nodes, and without this
|
|
55
|
-
* prop none are emitted.
|
|
77
|
+
* surucu:d1 — draw "Folders" and "Files" as labelled sections.
|
|
56
78
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
79
|
+
* ⚠ It used to arrive with `uiProfile: 'drive'`; the explorer now passes it
|
|
80
|
+
* unconditionally, because the sections are part of the shell rather than of
|
|
81
|
+
* a profile. The prop stays for an embedder mounting this view directly:
|
|
82
|
+
* absent/false renders the grid without them, down to the DOM — the two
|
|
83
|
+
* headings are the only extra nodes, and without this prop none are emitted.
|
|
84
|
+
*
|
|
85
|
+
* ⚠ The prop gates the HEADINGS, not the order. Folders come before files
|
|
86
|
+
* in every profile (see `ordered`) — a heading only names a group that is
|
|
87
|
+
* there either way.
|
|
61
88
|
*/
|
|
62
89
|
sections?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* gruplama — where these rows got their order, exactly as `ListView` takes
|
|
92
|
+
* it (`lib/sortOrder.ListingOrder`).
|
|
93
|
+
*
|
|
94
|
+
* ⚠ It changes nothing about the ORDER here — the pane hands this view rows
|
|
95
|
+
* that are already sorted, and `compareInOrder('relevance')` is
|
|
96
|
+
* byte-for-byte the folders-first pass below. It is read for one thing: a
|
|
97
|
+
* date heading over a RANKED answer would be a claim about an order the
|
|
98
|
+
* rows are not in. Omitted = an ordinary folder listing.
|
|
99
|
+
*/
|
|
100
|
+
order?: ListingOrder;
|
|
101
|
+
/**
|
|
102
|
+
* gorunum:v1-preview — we are inside an E2E-encrypted folder.
|
|
103
|
+
*
|
|
104
|
+
* Every body on the wire in there is ciphertext, so reading a file's first
|
|
105
|
+
* kilobytes to draw its first lines would spend a request to learn that the
|
|
106
|
+
* bytes do not decode. The text preview stays off; the cards keep their
|
|
107
|
+
* type tiles. Absent/false = the ordinary case.
|
|
108
|
+
*/
|
|
109
|
+
e2eActive?: boolean;
|
|
63
110
|
}>();
|
|
64
111
|
|
|
65
112
|
const emit = defineEmits<{
|
|
@@ -69,26 +116,83 @@ const emit = defineEmits<{
|
|
|
69
116
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
70
117
|
(e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
|
|
71
118
|
(e: 'star-change', node: FileNode, value: boolean): void;
|
|
119
|
+
/** gorunum:v1 — the order this view renders, so the parent's shift-range
|
|
120
|
+
* arithmetic runs over what the user sees rather than over the backend's
|
|
121
|
+
* answer. See the same emit in ListView. */
|
|
122
|
+
(e: 'display-order', nodes: FileNode[]): void;
|
|
72
123
|
}>();
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
/**
|
|
126
|
+
* gorunum:v1 — folders first, always, and by the SHARED comparator
|
|
127
|
+
* (`lib/listing.byFoldersFirst`) the list view uses as its own primary key.
|
|
128
|
+
* Two sorts, one rule: a second private copy of "dirs are 0, files are 1" is
|
|
129
|
+
* how the grid and the list end up disagreeing (filex lesson #67).
|
|
130
|
+
*
|
|
131
|
+
* `Array.prototype.sort` is stable, and this comparator returns 0 for any two
|
|
132
|
+
* nodes of the same kind, so whatever order the parent sends survives
|
|
133
|
+
* untouched INSIDE each group. The day a sort control reaches the grid,
|
|
134
|
+
* descending will mean folders descending and then files descending, without
|
|
135
|
+
* this line being touched.
|
|
136
|
+
*
|
|
137
|
+
* Display order only. Selection is keyed by path and the explorer's shift-range
|
|
138
|
+
* still walks its own list, so nothing here moves an index the parent holds.
|
|
139
|
+
*/
|
|
140
|
+
const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
|
|
141
|
+
useLocale(() => props.locale);
|
|
142
|
+
|
|
143
|
+
const ordered = computed<FileNode[]>(() => [...props.files].sort(byFoldersFirst));
|
|
144
|
+
watch(
|
|
145
|
+
ordered,
|
|
146
|
+
(list) => emit('display-order', list),
|
|
147
|
+
{ immediate: true },
|
|
80
148
|
);
|
|
149
|
+
|
|
81
150
|
const firstDirPath = computed(() => ordered.value.find((f) => f.type === 'dir')?.path ?? null);
|
|
82
151
|
const firstFilePath = computed(() => ordered.value.find((f) => f.type !== 'dir')?.path ?? null);
|
|
83
152
|
|
|
153
|
+
/* gruplama — the same sort this pane's list view reads, by injection, so the
|
|
154
|
+
* two cannot disagree about whether a date heading is honest right now. */
|
|
155
|
+
const sort = useSortStore();
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* gruplama — THE DATE HEADINGS, from `lib/dateGroups`, which is also where the
|
|
159
|
+
* list gets them. Not a grid variant of the ladder: a rung added there has to
|
|
160
|
+
* appear here on the same day or the two views name the same day differently.
|
|
161
|
+
*/
|
|
162
|
+
const grouped = computed(() =>
|
|
163
|
+
groupByDate(ordered.value, {
|
|
164
|
+
active: groupingActive(sort.key.value, props.order),
|
|
165
|
+
dateOf: (n) => toDate(n.last_modified),
|
|
166
|
+
/* Folders keep their own run at the top — the same rule the list follows,
|
|
167
|
+
* and the reason "folders before files" survives a date sort. Here the run
|
|
168
|
+
* is the one that already had a name. */
|
|
169
|
+
aside: (n) =>
|
|
170
|
+
n.type === 'dir'
|
|
171
|
+
? { id: 'dirs', label: props.sections ? t('drive.section.folders') : null }
|
|
172
|
+
: null,
|
|
173
|
+
labels: { t, formatMonthYear, zonedYearMonth },
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ⚠⚠ TWO HEADING SYSTEMS DO NOT STACK. While the rows are in date order the
|
|
179
|
+
* "Files" label is replaced by the date headings rather than sitting above
|
|
180
|
+
* them: the date headings ARE the files' headings, and "Files" followed
|
|
181
|
+
* immediately by "Today" names one group twice and tells the reader nothing
|
|
182
|
+
* the second time. "Folders" stays, because the folders are still one run and
|
|
183
|
+
* no date can name it.
|
|
184
|
+
*
|
|
185
|
+
* Off that order (any other sort key, or a ranked search answer) the grid
|
|
186
|
+
* keeps exactly the Folders / Files sections it has always drawn.
|
|
187
|
+
*/
|
|
84
188
|
function headingBefore(n: FileNode): string | null {
|
|
189
|
+
if (grouped.value.active) return grouped.value.headingBefore(n);
|
|
85
190
|
if (!props.sections) return null;
|
|
86
191
|
if (n.path === firstDirPath.value) return t('drive.section.folders');
|
|
87
192
|
if (n.path === firstFilePath.value) return t('drive.section.files');
|
|
88
193
|
return null;
|
|
89
194
|
}
|
|
90
195
|
|
|
91
|
-
const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
|
|
92
196
|
|
|
93
197
|
// Prefer the authenticated resolver when the host wired one; otherwise fall
|
|
94
198
|
// back to the raw URL (legacy same-origin behavior).
|
|
@@ -96,6 +200,61 @@ function thumbOf(n: FileNode): string | null {
|
|
|
96
200
|
return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
|
|
97
201
|
}
|
|
98
202
|
|
|
203
|
+
/* ==================================================================
|
|
204
|
+
* gorunum:v1-preview — the card shows the file's own first lines.
|
|
205
|
+
*
|
|
206
|
+
* ⚠ The brief for this slice said these files "have no thumbnail". Measured
|
|
207
|
+
* on the local backend, they do: `internal/thumb/generic.go` renders a card
|
|
208
|
+
* tinted from a hash of the extension with the letters "TS"/"CSV"/"ZIP" drawn
|
|
209
|
+
* in it, and the listing hands out a `thumb_url` for it (app.ts → a 1,745-byte
|
|
210
|
+
* JPEG). So the choice is not "preview or nothing", it is "the file's first
|
|
211
|
+
* lines, or a coloured rectangle repeating the extension the footer tile and
|
|
212
|
+
* the Type column both already say".
|
|
213
|
+
*
|
|
214
|
+
* Hence the order below: for a node we can read as TEXT the `<img>` is not
|
|
215
|
+
* rendered at all — which also means `thumbOf()` is never called for it and
|
|
216
|
+
* `useThumbs` never fetches that generic JPEG, so this costs the same one
|
|
217
|
+
* request it replaces. Every other node is untouched: images, video, audio
|
|
218
|
+
* and PDFs keep their real, content-derived thumbnails.
|
|
219
|
+
*
|
|
220
|
+
* While the read is in flight (and if it fails, or the bytes turn out not to
|
|
221
|
+
* be text) the card shows the type tile on `--fe-bg-elev` — the spec's stated
|
|
222
|
+
* fallback, and no flash of a placeholder we are about to replace.
|
|
223
|
+
* ================================================================== */
|
|
224
|
+
const previews = createFilePreviews({
|
|
225
|
+
// Read once: the explorer's `config.apiBase` is fixed for the life of a
|
|
226
|
+
// mounted panel, and `enabled` has to be answerable at construction.
|
|
227
|
+
apiBase: props.apiBase,
|
|
228
|
+
// Wrapped rather than passed, so a parent that re-creates its arrow on each
|
|
229
|
+
// render is still called through the CURRENT prop. ⚠ The builder is async;
|
|
230
|
+
// `createFilePreviews` awaits it (see the note there — a Promise spread into
|
|
231
|
+
// a headers object sends no Authorization and 401s in silence).
|
|
232
|
+
authHeaders: props.authHeaders ? () => props.authHeaders!() : undefined,
|
|
233
|
+
credentials: props.authCredentials,
|
|
234
|
+
disabled: () => props.e2eActive === true,
|
|
235
|
+
});
|
|
236
|
+
onBeforeUnmount(() => previews.dispose());
|
|
237
|
+
|
|
238
|
+
/** Non-null only for a node whose bytes we can read as text. */
|
|
239
|
+
function previewKind(n: FileNode) {
|
|
240
|
+
return previews.kindFor(n);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** At most one element, so the template gets a typed local without a
|
|
244
|
+
* non-null assertion repeated at every nested loop. */
|
|
245
|
+
function previewFor(n: FileNode): FilePreview[] {
|
|
246
|
+
const p = previews.get(n);
|
|
247
|
+
return p ? [p] : [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Template-ref sink: the preview box itself is what the IntersectionObserver
|
|
251
|
+
* watches, so nothing is fetched for a card that never comes into view.
|
|
252
|
+
* ⚠ `unknown` because a Vue template `:ref` hands back an Element, a
|
|
253
|
+
* component instance or null depending on what it is put on. */
|
|
254
|
+
function bindPreview(el: unknown, n: FileNode) {
|
|
255
|
+
previews.bind(el instanceof Element ? el : null, n);
|
|
256
|
+
}
|
|
257
|
+
|
|
99
258
|
/** A card carries a star when the host wired the API and the node is a file
|
|
100
259
|
* with a server id — the same rule the list row uses. */
|
|
101
260
|
function canStar(n: FileNode): boolean {
|
|
@@ -121,6 +280,17 @@ function onCtx(n: FileNode, ev: MouseEvent) {
|
|
|
121
280
|
emit('context-card', n, ev);
|
|
122
281
|
}
|
|
123
282
|
|
|
283
|
+
/**
|
|
284
|
+
* gorunum:v1 — the ⋮ button. It calls the RIGHT-CLICK handler with the same
|
|
285
|
+
* node and the same event, so the menu it opens is the context menu, built by
|
|
286
|
+
* the explorer's one `selectionActionList`. There is deliberately no action
|
|
287
|
+
* list here: a second one drifts from the first the week after it is written
|
|
288
|
+
* (filex lesson #67).
|
|
289
|
+
*/
|
|
290
|
+
function onMenuButton(n: FileNode, ev: MouseEvent) {
|
|
291
|
+
onCtx(n, ev);
|
|
292
|
+
}
|
|
293
|
+
|
|
124
294
|
function onItemDragStart(n: FileNode, ev: DragEvent) {
|
|
125
295
|
/* wiring:c4 — custom ghost (name + multi-select count badge), visual only. */
|
|
126
296
|
applyDragGhost(
|
|
@@ -178,15 +348,27 @@ function cancelPress() {
|
|
|
178
348
|
pressTarget = null;
|
|
179
349
|
}
|
|
180
350
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The card's second line. A file prints "1.7 MB • Sep 9, 2026"; a folder
|
|
353
|
+
* prints what it is, because its size is 0 and its date is the date something
|
|
354
|
+
* inside it changed.
|
|
355
|
+
*
|
|
356
|
+
* Same epoch normalization the list and the gallery use (the backend has sent
|
|
357
|
+
* both seconds and milliseconds), and the same locale the rest of the card is
|
|
358
|
+
* drawn in — `toLocaleDateString` with the explorer's own tag, not the
|
|
359
|
+
* browser's, so a Turkish panel says "9 Eyl 2026" whatever the OS is set to.
|
|
360
|
+
*/
|
|
361
|
+
function displayDate(ms: number | undefined): string {
|
|
362
|
+
return formatDate(ms);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function captionFor(n: FileNode): string {
|
|
366
|
+
if (n.type === 'dir') return t('node.folder');
|
|
367
|
+
const when = displayDate(n.last_modified);
|
|
368
|
+
const size = formatSize(n.size);
|
|
369
|
+
return when ? `${size} • ${when}` : size;
|
|
186
370
|
}
|
|
187
371
|
|
|
188
|
-
// Special rows keep their emoji (trash/storage are not file-TYPE icons);
|
|
189
|
-
// everything else renders the SVG icon set from lib/fileIcons.
|
|
190
372
|
function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
191
373
|
if (b === 'kept') return '\u2713';
|
|
192
374
|
if (b === 'syncing') return '\u27f3';
|
|
@@ -194,13 +376,6 @@ function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
|
194
376
|
return '\u2601';
|
|
195
377
|
}
|
|
196
378
|
|
|
197
|
-
function specialEmojiFor(n: FileNode): string | null {
|
|
198
|
-
if (n.basename === '.trash') return '🗑';
|
|
199
|
-
if (n.mime_type === 'inode/storage') return '💾';
|
|
200
|
-
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
379
|
/* bul:s3 — search-result enrichment, same presence-gating as ListView:
|
|
205
380
|
* only search hits carry `snippet`, so normal listings render nothing. */
|
|
206
381
|
function cardSnippet(n: FileNode): string {
|
|
@@ -236,6 +411,8 @@ function snippetTitle(snippet: string): string {
|
|
|
236
411
|
<div
|
|
237
412
|
class="fe-grid__card"
|
|
238
413
|
:class="{
|
|
414
|
+
'fe-grid__card--folder': n.type === 'dir' /* gorunum:v1 */,
|
|
415
|
+
'fe-grid__card--file': n.type !== 'dir' /* gorunum:v1 */,
|
|
239
416
|
'is-selected': isSelected(n),
|
|
240
417
|
'is-dir': n.type === 'dir',
|
|
241
418
|
'is-trash': n.trashed,
|
|
@@ -259,7 +436,49 @@ function snippetTitle(snippet: string): string {
|
|
|
259
436
|
@touchend="cancelPress"
|
|
260
437
|
@touchmove="cancelPress"
|
|
261
438
|
>
|
|
262
|
-
|
|
439
|
+
<!-- gorunum:v1 — the preview, files only: 184×108. A thumbnail when
|
|
440
|
+
there is one, otherwise the type tile centred on --fe-bg-elev.
|
|
441
|
+
gorunum:v1-preview — and, for a file we can read as text, its own
|
|
442
|
+
first lines instead (see the block comment in the script). -->
|
|
443
|
+
<div v-if="n.type !== 'dir'" class="fe-grid__thumb" :ref="(el) => bindPreview(el, n)">
|
|
444
|
+
<!-- gorunum:v1-preview — text-readable kinds never reach the <img>
|
|
445
|
+
branch, so no generic placeholder thumbnail is fetched for them. -->
|
|
446
|
+
<template v-if="previewKind(n)">
|
|
447
|
+
<!-- ⚠ Interpolated, never v-html: this is somebody's file content. -->
|
|
448
|
+
<div
|
|
449
|
+
v-for="p in previewFor(n)"
|
|
450
|
+
:key="'fprev'"
|
|
451
|
+
class="fe-fprev"
|
|
452
|
+
:class="'fe-fprev--' + p.kind"
|
|
453
|
+
:style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
|
|
454
|
+
aria-hidden="true"
|
|
455
|
+
>
|
|
456
|
+
<!-- One grid, cells emitted flat: <template> makes no DOM, so a
|
|
457
|
+
short row's padding cells keep every column aligned. -->
|
|
458
|
+
<template v-if="p.kind === 'table'">
|
|
459
|
+
<template v-for="(row, ri) in p.rows" :key="ri">
|
|
460
|
+
<span
|
|
461
|
+
v-for="(cell, ci) in row"
|
|
462
|
+
:key="ci"
|
|
463
|
+
class="fe-fprev__cell"
|
|
464
|
+
:class="{ 'is-head': ri === 0 }"
|
|
465
|
+
>{{ cell }}</span>
|
|
466
|
+
</template>
|
|
467
|
+
</template>
|
|
468
|
+
<template v-else>
|
|
469
|
+
<div v-for="(ln, li) in p.lines" :key="li" class="fe-fprev__line"><span
|
|
470
|
+
v-if="ln.tint"
|
|
471
|
+
class="fe-fprev__tint"
|
|
472
|
+
>{{ ln.indent }}{{ ln.tint }}</span>{{ ln.text }}</div>
|
|
473
|
+
</template>
|
|
474
|
+
</div>
|
|
475
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
476
|
+
<span
|
|
477
|
+
v-if="previewFor(n).length === 0"
|
|
478
|
+
class="fe-grid__icon fe-grid__icon--svg"
|
|
479
|
+
v-html="fileIconTile(n)"
|
|
480
|
+
></span>
|
|
481
|
+
</template>
|
|
263
482
|
<!--
|
|
264
483
|
draggable="false" — HTML5 image drag dataTransfer adds 'Files'
|
|
265
484
|
MIME, which trips the parent's upload handler; without this
|
|
@@ -267,20 +486,41 @@ function snippetTitle(snippet: string): string {
|
|
|
267
486
|
Parent card stays draggable=true so internal move works.
|
|
268
487
|
-->
|
|
269
488
|
<img
|
|
270
|
-
v-if="thumbOf(n)"
|
|
489
|
+
v-else-if="thumbOf(n)"
|
|
271
490
|
:src="thumbOf(n)!"
|
|
272
491
|
:alt="n.basename"
|
|
492
|
+
:class="{ 'fe-thumb--page': drawsAsPage(n) /* gorunum:v1-preview — crop a page from its TOP */ }"
|
|
273
493
|
loading="lazy"
|
|
274
494
|
draggable="false"
|
|
275
495
|
/>
|
|
276
|
-
|
|
496
|
+
<!-- ikon:emoji — an encrypted folder is still a FOLDER, so it keeps the
|
|
497
|
+
folder's own shape and colour with the padlock cut out of it; the
|
|
498
|
+
🔒 it replaces said "locked" and nothing else. One definition,
|
|
499
|
+
in lib/fileIcons, for all three views. -->
|
|
500
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
501
|
+
<span
|
|
502
|
+
v-else-if="isEncryptedFolder(n)"
|
|
503
|
+
class="fe-grid__icon fe-grid__icon--svg"
|
|
504
|
+
role="img"
|
|
505
|
+
:aria-label="t('e2e.badge')"
|
|
506
|
+
v-html="encryptedFolderTile()"
|
|
507
|
+
></span>
|
|
277
508
|
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
278
|
-
<span v-else class="fe-grid__icon fe-grid__icon--svg" v-html="
|
|
509
|
+
<span v-else class="fe-grid__icon fe-grid__icon--svg" v-html="fileIconTile(n)"></span>
|
|
279
510
|
<!-- Star, ON the tile. A hover-only affordance would be invisible to
|
|
280
511
|
the person looking for what they starred, so the chip is always
|
|
281
512
|
painted once the file IS starred and only appears on hover/focus
|
|
282
513
|
otherwise (see .fe-grid__star in styles/base.css). @click.stop so
|
|
283
514
|
starring never doubles as a card selection. -->
|
|
515
|
+
<!-- gorunum:v1-preview — a frame lifted out of a video is, on a card,
|
|
516
|
+
indistinguishable from a photograph. The badge is the difference,
|
|
517
|
+
and it is drawn only over a real frame: a video that fell back to
|
|
518
|
+
its type tile already says what it is. -->
|
|
519
|
+
<span
|
|
520
|
+
v-if="!previewKind(n) && thumbOf(n) && drawsAsVideo(n)"
|
|
521
|
+
class="fe-thumb__play"
|
|
522
|
+
aria-hidden="true"
|
|
523
|
+
></span>
|
|
284
524
|
<div v-if="canStar(n)" class="fe-grid__star" @click.stop @dblclick.stop>
|
|
285
525
|
<StarButton
|
|
286
526
|
:starred="!!starredIds?.has(n.id!)"
|
|
@@ -295,30 +535,54 @@ function snippetTitle(snippet: string): string {
|
|
|
295
535
|
/>
|
|
296
536
|
</div>
|
|
297
537
|
</div>
|
|
298
|
-
|
|
299
|
-
|
|
538
|
+
|
|
539
|
+
<!-- gorunum:v1 — the row both cards share: tile · name over caption · ⋮.
|
|
540
|
+
On a file it is the 56px footer under the preview; on a folder it is
|
|
541
|
+
the whole 56px card. -->
|
|
542
|
+
<div class="fe-grid__foot">
|
|
543
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
300
544
|
<span
|
|
301
|
-
v-if="
|
|
302
|
-
|
|
303
|
-
:title="t('keep.badge_' + keepBadgeFor(n))"
|
|
545
|
+
v-if="isEncryptedFolder(n)"
|
|
546
|
+
class="fe-grid__tile fe-grid__tile--svg"
|
|
304
547
|
role="img"
|
|
305
|
-
:aria-label="t('
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
v-
|
|
310
|
-
class="fe-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
548
|
+
:aria-label="t('e2e.badge')"
|
|
549
|
+
v-html="encryptedFolderTile()"
|
|
550
|
+
></span>
|
|
551
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
552
|
+
<span v-else class="fe-grid__tile fe-grid__tile--svg" v-html="fileIconTile(n)"></span>
|
|
553
|
+
<div class="fe-grid__main">
|
|
554
|
+
<div class="fe-grid__label" :title="n.basename">
|
|
555
|
+
{{ nodeDisplayName(n) }}
|
|
556
|
+
<span
|
|
557
|
+
v-if="keepBadgeFor && keepBadgeFor(n)"
|
|
558
|
+
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
|
|
559
|
+
:title="t('keep.badge_' + keepBadgeFor(n))"
|
|
560
|
+
role="img"
|
|
561
|
+
:aria-label="t('keep.badge_' + keepBadgeFor(n))"
|
|
562
|
+
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="fe-grid__meta">{{ captionFor(n) }}</div>
|
|
565
|
+
<div
|
|
566
|
+
v-if="showParentPath"
|
|
567
|
+
class="fe-grid__parent"
|
|
568
|
+
:title="parentDirOf(n.path)"
|
|
569
|
+
>{{ parentDirOf(n.path) || '—' }}</div>
|
|
570
|
+
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
571
|
+
<div v-if="cardSnippet(n)" class="fe-grid__snippet" :title="snippetTitle(cardSnippet(n))">
|
|
572
|
+
<template v-for="(seg, si) in snippetSegments(cardSnippet(n))" :key="si">
|
|
573
|
+
<mark v-if="seg.match" class="fe-grid__mark">{{ seg.text }}</mark>
|
|
574
|
+
<template v-else>{{ seg.text }}</template>
|
|
575
|
+
</template>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
class="fe-grid__menu"
|
|
581
|
+
:aria-label="t('toolbar.more')"
|
|
582
|
+
:title="t('toolbar.more')"
|
|
583
|
+
@click.stop="onMenuButton(n, $event)"
|
|
584
|
+
@dblclick.stop
|
|
585
|
+
>⋮</button>
|
|
322
586
|
</div>
|
|
323
587
|
</div>
|
|
324
588
|
</template>
|