@brftech/filex-core 0.40.0 → 0.41.1

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 (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
  3. package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
  4. package/dist/CsvViewer-CyuwENI_.js +141 -0
  5. package/dist/CsvViewer-CyuwENI_.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
  7. package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
  8. package/dist/EpubViewer-K9PjCMmS.js +163 -0
  9. package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
  10. package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
  11. package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
  12. package/dist/MermaidViewer-Bp73aId8.js +137 -0
  13. package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
  14. package/dist/PsdViewer-C2eLivw1.js +122 -0
  15. package/dist/PsdViewer-C2eLivw1.js.map +1 -0
  16. package/dist/TiffViewer-DsHg5gcz.js +142 -0
  17. package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
  18. package/dist/Viewer3D-C_dc2HN9.js +75 -0
  19. package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
  20. package/dist/filex-core.js +218 -20361
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-vxMZpYb-.js +26700 -0
  25. package/dist/index-vxMZpYb-.js.map +1 -0
  26. package/dist/index.d.ts +4212 -862
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2154 -506
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +39 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +951 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +172 -56
  39. package/src/components/GridView.vue +331 -82
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1173 -194
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +26 -1
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +6 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +135 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +35 -14
  60. package/src/components/Toolbar.vue +953 -465
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +89 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useLocale.ts +382 -26
  66. package/src/composables/useRowTouch.ts +95 -0
  67. package/src/index.ts +177 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +31 -2
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +151 -116
  80. package/src/lib/timezone.ts +501 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +416 -26
  84. package/src/locales/tr.ts +409 -26
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +557 -476
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5409 -8
  93. package/src/styles/variables.css +139 -65
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. package/src/modals/ShareModal.vue +0 -139
@@ -8,14 +8,27 @@
8
8
  * square tiles: thumbnail (or SVG file-type icon fallback) with the name
9
9
  * below and size+date revealed on hover/focus. GridView itself is untouched.
10
10
  */
11
- import { ref } from 'vue';
11
+ import { computed, onBeforeUnmount, ref } from 'vue';
12
12
  import { hasInternalDrag } from '../lib/dragOut';
13
13
  import type { FileNode } from '../types/FileNode';
14
14
  import type { LocaleCode } from '../types/ExplorerConfig';
15
15
  import { useLocale } from '../composables/useLocale';
16
- import { fileIconSvg } from '../lib/fileIcons';
16
+ import { useRowTouch } from '../composables/useRowTouch';
17
+ import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
18
+ import {
19
+ createFilePreviews,
20
+ drawsAsPage,
21
+ drawsAsVideo,
22
+ type FilePreview,
23
+ } from '../lib/filePreview'; /* gorunum:v1-preview */
17
24
  import StarButton from './StarButton.vue';
18
25
  import { applyDragGhost } from '../lib/dragGhost';
26
+ import { parentDirOf } from '../lib/listing'; /* the folder a row sits in — one split rule for every view */
27
+ import {
28
+ groupByDate,
29
+ groupingActive,
30
+ } from '../lib/dateGroups'; /* gruplama */
31
+ import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama */
19
32
 
20
33
  const props = defineProps<{
21
34
  files: FileNode[];
@@ -50,10 +63,21 @@ const props = defineProps<{
50
63
  apiBase?: string;
51
64
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
52
65
  authCredentials?: RequestCredentials;
66
+ /** gorunum:v1-preview — inside an E2E-encrypted folder every body is
67
+ * ciphertext, so the text preview stays off. Same prop, same meaning and
68
+ * same default as GridView's. */
69
+ e2eActive?: boolean;
70
+ /**
71
+ * gruplama — where these rows got their order, the same prop the list and
72
+ * the grid take (`lib/sortOrder.ListingOrder`). Read for one thing only: a
73
+ * date heading over the server's RANKED answer would be a claim about an
74
+ * order the tiles are not in. Omitted = an ordinary folder listing.
75
+ */
76
+ order?: ListingOrder;
53
77
  }>();
54
78
 
55
79
  const emit = defineEmits<{
56
- (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
80
+ (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean; touch?: boolean }): void;
57
81
  (e: 'dbl-card', node: FileNode): void;
58
82
  (e: 'context-card', node: FileNode, ev: MouseEvent): void;
59
83
  (e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
@@ -61,12 +85,68 @@ const emit = defineEmits<{
61
85
  (e: 'star-change', node: FileNode, value: boolean): void;
62
86
  }>();
63
87
 
64
- const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
88
+ const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
89
+ useLocale(() => props.locale);
90
+
91
+ /* gruplama — this pane's sort, by injection, so the gallery cannot disagree
92
+ * with the list beside it about whether a date heading is honest right now. */
93
+ const sort = useSortStore();
94
+
95
+ /**
96
+ * gruplama — THE DATE HEADINGS, from the one module that owns the ladder
97
+ * (`lib/dateGroups`), read identically by ListView and GridView.
98
+ *
99
+ * ⚠ The gallery has no Folders / Files sections to collide with, so nothing
100
+ * here replaces anything: folders keep a run of their own at the top — with no
101
+ * heading, because the gallery has never named one — and each day of files
102
+ * gets its label. Any sort key but `modified` draws nothing at all, exactly as
103
+ * before this existed.
104
+ *
105
+ * ⚠ The tiles are handed to us ALREADY sorted (`FilePane.displayFiles` runs
106
+ * the pane's comparator, folders first, then the key), which is why this
107
+ * groups `props.files` in place rather than re-sorting: a second sort here is
108
+ * a second opinion about the order, and the grid learned that lesson once.
109
+ */
110
+ const grouped = computed(() =>
111
+ groupByDate(props.files, {
112
+ active: groupingActive(sort.key.value, props.order),
113
+ dateOf: (n) => toDate(n.last_modified),
114
+ aside: (n) => (n.type === 'dir' ? { id: 'dirs' } : null),
115
+ labels: { t, formatMonthYear, zonedYearMonth },
116
+ }),
117
+ );
65
118
 
66
119
  function thumbOf(n: FileNode): string | null {
67
120
  return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
68
121
  }
69
122
 
123
+ /* gorunum:v1-preview — the SAME preview the grid card draws, from the same
124
+ * module and with the same bounds. Not a gallery variant: a text file in a
125
+ * gallery is the identical hole (the backend's generic extension-card), and
126
+ * "open it on the desktop, leave the embeds on the old behaviour" is how one
127
+ * product becomes two. Only the type scale differs, and that is CSS. */
128
+ const previews = createFilePreviews({
129
+ apiBase: props.apiBase,
130
+ authHeaders: props.authHeaders ? () => props.authHeaders!() : undefined,
131
+ credentials: props.authCredentials,
132
+ disabled: () => props.e2eActive === true,
133
+ });
134
+ onBeforeUnmount(() => previews.dispose());
135
+
136
+ function previewKind(n: FileNode) {
137
+ return previews.kindFor(n);
138
+ }
139
+
140
+ /** At most one element — a typed local for the template, no `!` per loop. */
141
+ function previewFor(n: FileNode): FilePreview[] {
142
+ const p = previews.get(n);
143
+ return p ? [p] : [];
144
+ }
145
+
146
+ function bindPreview(el: unknown, n: FileNode) {
147
+ previews.bind(el instanceof Element ? el : null, n);
148
+ }
149
+
70
150
  /** A card carries a star when the host wired the API and the node is a file
71
151
  * with a server id — the same rule the list row uses. */
72
152
  function canStar(n: FileNode): boolean {
@@ -79,7 +159,7 @@ function isSelected(n: FileNode): boolean {
79
159
  }
80
160
 
81
161
  function onClick(n: FileNode, ev: MouseEvent) {
82
- emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey });
162
+ emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey, touch: touch.isTap(ev) });
83
163
  }
84
164
 
85
165
  function onDbl(n: FileNode) {
@@ -126,47 +206,20 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
126
206
  emit('item-drop-into', n, ev);
127
207
  }
128
208
 
129
- // Long-press → context menu, same as GridView (touch parity).
130
- let pressTimer: ReturnType<typeof setTimeout> | undefined;
131
- let pressTarget: FileNode | null = null;
132
- function onTouchStart(n: FileNode, ev: TouchEvent) {
133
- pressTarget = n;
134
- if (pressTimer) clearTimeout(pressTimer);
135
- pressTimer = setTimeout(() => {
136
- if (pressTarget) {
137
- const t0 = ev.touches[0];
138
- emit('context-card', pressTarget, {
139
- clientX: t0.clientX,
140
- clientY: t0.clientY,
141
- preventDefault: () => {},
142
- } as unknown as MouseEvent);
143
- }
144
- }, 500);
145
- }
146
- function cancelPress() {
147
- if (pressTimer) clearTimeout(pressTimer);
148
- pressTarget = null;
149
- }
150
-
151
- function parentDir(path: string): string {
152
- const stripped = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
153
- const idx = stripped.lastIndexOf('/');
154
- if (idx === -1) return '';
155
- return stripped.slice(0, idx);
156
- }
157
-
158
- // Special rows keep their emoji (trash/storage are not file-TYPE icons).
159
- function specialEmojiFor(n: FileNode): string | null {
160
- if (n.basename === '.trash') return '🗑';
161
- if (n.mime_type === 'inode/storage') return '💾';
162
- if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
163
- return null;
164
- }
209
+ /* Long press → the card's menu; a tap is reported as a tap (issue #26). */
210
+ const touch = useRowTouch<FileNode>((n, at) =>
211
+ emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
212
+ );
165
213
 
214
+ /**
215
+ * zaman:z1 — routed through useLocale.formatDate, the package's one date
216
+ * formatter. It was a bare `toLocaleString()`, i.e. the BROWSER's locale and
217
+ * the BROWSER's zone: the same file's timestamp came out in a different
218
+ * language here than in the list two clicks away, and in a different clock
219
+ * than the viewer had chosen. Neither of those is a gallery decision.
220
+ */
166
221
  function displayDate(ms: number | undefined): string {
167
- if (!ms) return '';
168
- const d = new Date(ms * (ms < 1e12 ? 1000 : 1));
169
- return d.toLocaleString();
222
+ return formatDate(ms, { time: true });
170
223
  }
171
224
 
172
225
  // Hover meta line: size for files, entry count (when known) for dirs.
@@ -186,9 +239,13 @@ function metaFor(n: FileNode): string {
186
239
  :aria-label="t('gallery.aria')"
187
240
  :aria-busy="loading ? 'true' : undefined"
188
241
  >
242
+ <template v-for="n in files" :key="n.path">
243
+ <!-- gruplama — the date label: a grid item spanning every column, so the
244
+ tiles keep flowing under it. The cards keep `role="option"`; the
245
+ heading is aria-hidden and the day it names is already in each
246
+ card's own hover text. -->
247
+ <p v-if="grouped.headingBefore(n)" class="fe-gal__heading" aria-hidden="true">{{ grouped.headingBefore(n) }}</p>
189
248
  <div
190
- v-for="n in files"
191
- :key="n.path"
192
249
  class="fe-gal__card"
193
250
  :class="{
194
251
  'is-selected': isSelected(n),
@@ -210,27 +267,85 @@ function metaFor(n: FileNode): string {
210
267
  @dragover="onItemDragOver(n, $event)"
211
268
  @dragleave="onItemDragLeave(n)"
212
269
  @drop="onItemDrop(n, $event)"
213
- @touchstart.passive="onTouchStart(n, $event)"
214
- @touchend="cancelPress"
215
- @touchmove="cancelPress"
270
+ @touchstart.passive="touch.onTouchStart(n, $event)"
271
+ @touchend.passive="touch.onTouchEnd"
272
+ @touchmove.passive="touch.onTouchMove"
216
273
  >
217
- <div class="fe-gal__thumb">
274
+ <div class="fe-gal__thumb" :ref="(el) => bindPreview(el, n)">
275
+ <!-- gorunum:v1-preview — a text-readable file shows its own first
276
+ lines; it never reaches the <img> branch, so no generic
277
+ placeholder thumbnail is fetched for it either. -->
278
+ <template v-if="previewKind(n)">
279
+ <!-- ⚠ Interpolated, never v-html: this is somebody's file content. -->
280
+ <div
281
+ v-for="p in previewFor(n)"
282
+ :key="'fprev'"
283
+ class="fe-fprev"
284
+ :class="'fe-fprev--' + p.kind"
285
+ :style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
286
+ aria-hidden="true"
287
+ >
288
+ <!-- One grid, cells emitted flat: <template> makes no DOM, so a
289
+ short row's padding cells keep every column aligned. -->
290
+ <template v-if="p.kind === 'table'">
291
+ <template v-for="(row, ri) in p.rows" :key="ri">
292
+ <span
293
+ v-for="(cell, ci) in row"
294
+ :key="ci"
295
+ class="fe-fprev__cell"
296
+ :class="{ 'is-head': ri === 0 }"
297
+ >{{ cell }}</span>
298
+ </template>
299
+ </template>
300
+ <template v-else>
301
+ <div v-for="(ln, li) in p.lines" :key="li" class="fe-fprev__line"><span
302
+ v-if="ln.tint"
303
+ class="fe-fprev__tint"
304
+ >{{ ln.indent }}{{ ln.tint }}</span>{{ ln.text }}</div>
305
+ </template>
306
+ </div>
307
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
308
+ <span
309
+ v-if="previewFor(n).length === 0"
310
+ class="fe-gal__icon fe-gal__icon--svg"
311
+ v-html="fileIconTile(n)"
312
+ ></span>
313
+ </template>
218
314
  <!-- draggable="false" for the same reason as GridView: HTML5 image
219
315
  drag adds a 'Files' MIME to dataTransfer, which would trip the
220
316
  parent's upload handler on internal drags. -->
221
317
  <img
222
- v-if="thumbOf(n)"
318
+ v-else-if="thumbOf(n)"
223
319
  :src="thumbOf(n)!"
224
320
  :alt="n.basename"
321
+ :class="{ 'fe-thumb--page': drawsAsPage(n) /* gorunum:v1-preview — crop a page from its TOP */ }"
225
322
  loading="lazy"
226
323
  draggable="false"
227
324
  />
228
- <span v-else-if="specialEmojiFor(n)" class="fe-gal__icon">{{ specialEmojiFor(n) }}</span>
325
+ <!-- ikon:emoji see GridView: the folder shape survives, the padlock
326
+ is cut out of it, and the name the 🔒 never had is on the span. -->
327
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
328
+ <span
329
+ v-else-if="isEncryptedFolder(n)"
330
+ class="fe-gal__icon fe-gal__icon--svg"
331
+ role="img"
332
+ :aria-label="t('e2e.badge')"
333
+ v-html="encryptedFolderTile()"
334
+ ></span>
229
335
  <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
230
- <span v-else class="fe-gal__icon fe-gal__icon--svg" v-html="fileIconSvg(n)"></span>
336
+ <span v-else class="fe-gal__icon fe-gal__icon--svg" v-html="fileIconTile(n)"></span>
231
337
  <!-- Same star chip as the grid, same component, same rule: painted
232
338
  when starred, on hover/focus otherwise. It sits above .fe-gal__meta
233
339
  (which is aria-hidden and covers the tile's foot on hover). -->
340
+ <!-- gorunum:v1-preview — a frame lifted out of a video is, on a card,
341
+ indistinguishable from a photograph. The badge is the difference,
342
+ and it is drawn only over a real frame: a video that fell back to
343
+ its type tile already says what it is. -->
344
+ <span
345
+ v-if="!previewKind(n) && thumbOf(n) && drawsAsVideo(n)"
346
+ class="fe-thumb__play"
347
+ aria-hidden="true"
348
+ ></span>
234
349
  <div v-if="canStar(n)" class="fe-gal__star" @click.stop @dblclick.stop>
235
350
  <StarButton
236
351
  :starred="!!starredIds?.has(n.id!)"
@@ -247,16 +362,17 @@ function metaFor(n: FileNode): string {
247
362
  <div class="fe-gal__meta" aria-hidden="true">
248
363
  <span v-if="metaFor(n)" class="fe-gal__meta-line">{{ metaFor(n) }}</span>
249
364
  <span
250
- v-if="showParentPath && parentDir(n.path)"
365
+ v-if="showParentPath && parentDirOf(n.path)"
251
366
  class="fe-gal__meta-line fe-gal__meta-line--path"
252
- :title="parentDir(n.path)"
253
- >{{ parentDir(n.path) }}</span>
367
+ :title="parentDirOf(n.path)"
368
+ >{{ parentDirOf(n.path) }}</span>
254
369
  </div>
255
370
  </div>
256
371
  <div class="fe-gal__label" :title="n.basename">
257
372
  {{ nodeDisplayName(n) }}
258
373
  </div>
259
374
  </div>
375
+ </template>
260
376
  <div v-if="!loading && files.length === 0" class="fe-gal__empty">
261
377
  {{ t('empty.folder') }}
262
378
  </div>