@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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* HomeView — the overview the explorer lands on, INSIDE the shell.
|
|
4
|
+
*
|
|
5
|
+
* Three blocks, each answering a question somebody actually has on arrival:
|
|
6
|
+
*
|
|
7
|
+
* Storages which drives are mine to open? (one card per storage)
|
|
8
|
+
* Recent what was I just working on?
|
|
9
|
+
* Starred what did I mark to come back to?
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ Why it is in this package and not in the web app. It used to be a
|
|
12
|
+
* separate PAGE there (`web/src/views/Home.vue`) with a header of its own — a
|
|
13
|
+
* logo, a refresh, an "All files" button, an apps grid, sign-out and a
|
|
14
|
+
* language switch — and no navigation panel at all, so moving between Home and
|
|
15
|
+
* the files replaced the whole screen and half of that header repeated a
|
|
16
|
+
* control the explorer already drew. Home is a VIEW, the way Recent and
|
|
17
|
+
* Starred are: same panel, same top bar, only the content area changes. A view
|
|
18
|
+
* of the explorer belongs to the explorer, and putting it here is also what
|
|
19
|
+
* makes the panel's "Home" row mean something inside `<filex-explorer>`
|
|
20
|
+
* instead of being a destination that only exists on one surface.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ Recent and Starred render through GridView — the SAME card, with the same
|
|
23
|
+
* thumbnails, the same type tiles and the same right-click menu as the listing
|
|
24
|
+
* behind it. A second card implementation for the same object is how the two
|
|
25
|
+
* start disagreeing about what a .docx looks like (it already happened once:
|
|
26
|
+
* the web page deep-imported `fileIcons` precisely to avoid it).
|
|
27
|
+
*
|
|
28
|
+
* ⚠ No `<style>` block, scoped or otherwise — the package's CSS lives in
|
|
29
|
+
* `styles/base.css`. A scoped block compiles to `.cls[data-v-HASH]` and the
|
|
30
|
+
* hash does not match in the web-component build, so the rules would silently
|
|
31
|
+
* stop applying in every embed.
|
|
32
|
+
*/
|
|
33
|
+
import { computed, ref } from 'vue';
|
|
34
|
+
import type { FileNode } from '../types/FileNode';
|
|
35
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
36
|
+
import { useLocale } from '../composables/useLocale';
|
|
37
|
+
import { fileIconTile } from '../lib/fileIcons';
|
|
38
|
+
import { nameMatches } from '../lib/fileFilters';
|
|
39
|
+
import GridView from './GridView.vue';
|
|
40
|
+
|
|
41
|
+
export interface HomeStorage {
|
|
42
|
+
name: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
driver?: string;
|
|
45
|
+
readOnly?: boolean;
|
|
46
|
+
/** Bytes this storage holds, when the server reported one. */
|
|
47
|
+
usedBytes?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const props = defineProps<{
|
|
51
|
+
storages: HomeStorage[];
|
|
52
|
+
recent: FileNode[];
|
|
53
|
+
starred: FileNode[];
|
|
54
|
+
/** True while the two per-user lists are still in flight. */
|
|
55
|
+
loading?: boolean;
|
|
56
|
+
locale: LocaleCode;
|
|
57
|
+
/**
|
|
58
|
+
* surucu:d1-scope — what the filter row above this view has in its name box.
|
|
59
|
+
*
|
|
60
|
+
* ⚠ It narrows ALL THREE blocks, not the two made of files. Home's first
|
|
61
|
+
* block is the drive list, and "orada adam isterse storage ismi aratabilir"
|
|
62
|
+
* is precisely the case the owner asked for; a box that skipped the block
|
|
63
|
+
* with the fewest, longest-lived names in it would be the box that misses
|
|
64
|
+
* the one thing people look for here.
|
|
65
|
+
*
|
|
66
|
+
* ⚠ Through `lib/fileFilters`'s own `nameMatches`, which is the predicate the
|
|
67
|
+
* listing's box uses — so an accent folds here exactly as it folds there.
|
|
68
|
+
* Optional: an embedder that draws no filter row passes nothing and sees
|
|
69
|
+
* everything, which is what this view has always done.
|
|
70
|
+
*/
|
|
71
|
+
nameFilter?: string;
|
|
72
|
+
/** Authenticated thumb resolver (useThumbs.src) — the grid needs it in an
|
|
73
|
+
* embed, where a bare root-relative `thumb_url` is not authenticated. */
|
|
74
|
+
thumbSrc?: (n: FileNode) => string | null;
|
|
75
|
+
}>();
|
|
76
|
+
|
|
77
|
+
const emit = defineEmits<{
|
|
78
|
+
(e: 'open-storage', name: string): void;
|
|
79
|
+
(e: 'open-node', node: FileNode): void;
|
|
80
|
+
(e: 'context-node', node: FileNode, ev: MouseEvent): void;
|
|
81
|
+
}>();
|
|
82
|
+
|
|
83
|
+
const { t, formatSize } = useLocale(() => props.locale);
|
|
84
|
+
|
|
85
|
+
/* surucu:d1-scope — the three narrowed blocks.
|
|
86
|
+
*
|
|
87
|
+
* ⚠ A storage is matched on the word ON THE CARD (`label || name`), which is
|
|
88
|
+
* also the only word a person can see to type. A card labelled "Arşiv" over a
|
|
89
|
+
* storage named `s3-archive` must answer to "arş"; matching the wire name
|
|
90
|
+
* instead would make the filter miss the thing it is pointing at.
|
|
91
|
+
*/
|
|
92
|
+
const needle = computed(() => (props.nameFilter ?? '').trim());
|
|
93
|
+
const filtering = computed(() => needle.value !== '');
|
|
94
|
+
const shownStorages = computed(() =>
|
|
95
|
+
props.storages.filter((s) => nameMatches(s.label || s.name, needle.value)),
|
|
96
|
+
);
|
|
97
|
+
const shownRecent = computed(() =>
|
|
98
|
+
props.recent.filter((n) => nameMatches(n.basename || '', needle.value)),
|
|
99
|
+
);
|
|
100
|
+
const shownStarred = computed(() =>
|
|
101
|
+
props.starred.filter((n) => nameMatches(n.basename || '', needle.value)),
|
|
102
|
+
);
|
|
103
|
+
/** True when a block has rows but the filter hid all of them — a different
|
|
104
|
+
* fact from "you have starred nothing", and it must not borrow that sentence. */
|
|
105
|
+
function filteredEmpty(total: number, shown: number): boolean {
|
|
106
|
+
return filtering.value && total > 0 && shown === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ⚠ One frozen empty set, not `new Set()` in the template. GridView takes the
|
|
111
|
+
* selection as a prop and a fresh Set on every render is a new identity every
|
|
112
|
+
* time, which re-runs everything downstream of it for a value that never
|
|
113
|
+
* changes. Nothing on Home is selectable: there is no selection bar, no
|
|
114
|
+
* checkbox column and no verb to apply — so the honest answer is "nothing",
|
|
115
|
+
* once.
|
|
116
|
+
*/
|
|
117
|
+
const NO_SELECTION: Set<string> = new Set();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The storage card's glyph.
|
|
121
|
+
*
|
|
122
|
+
* ⚠ `mime_type: 'inode/storage'` is the whole point of this line. A storage row
|
|
123
|
+
* IS `type: 'dir'`, so asking for `{ type: 'dir' }` alone falls through to the
|
|
124
|
+
* plain folder branch and every drive on the first screen a person sees — the
|
|
125
|
+
* screen with the most drives on it — came out as a yellow folder. `storage` is
|
|
126
|
+
* a real icon family now (lib/fileIcons answers it BEFORE the folder branch),
|
|
127
|
+
* and it is the two-bay drive the navigation panel already draws for the same
|
|
128
|
+
* storages: one shape for a drive across the panel, the list, the grid and here.
|
|
129
|
+
*
|
|
130
|
+
* One constant rather than a per-card call: every storage resolves to the same
|
|
131
|
+
* family, so this is one string, not one per row.
|
|
132
|
+
*/
|
|
133
|
+
const storageTile = fileIconTile({ type: 'dir', mime_type: 'inode/storage' });
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What a storage card says under its name.
|
|
137
|
+
*
|
|
138
|
+
* ⚠ A size only when the server reported one, and it is the same quantity for
|
|
139
|
+
* everybody who gets it (`/api/admin/storages` for an operator, the
|
|
140
|
+
* RBAC-filtered `/api/files/quota/storages` for everybody else). The per-USER
|
|
141
|
+
* quota is a sum across every storage, so printing that here would be a number
|
|
142
|
+
* about the person under a label about the drive. With no figure the caption
|
|
143
|
+
* names the kind of thing instead: a card with no size line says less; a card
|
|
144
|
+
* with the wrong size line says something false.
|
|
145
|
+
*/
|
|
146
|
+
function storageCaption(s: HomeStorage): string {
|
|
147
|
+
return typeof s.usedBytes === 'number' && s.usedBytes >= 0
|
|
148
|
+
? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
|
|
149
|
+
: t('conn.guide.storage');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A card opens what it names.
|
|
154
|
+
*
|
|
155
|
+
* ⚠ SINGLE click, and that is not an inconsistency with the listing behind it.
|
|
156
|
+
* A card in the listing has two jobs — click selects, double click opens — and
|
|
157
|
+
* on Home the first of those does not exist (see NO_SELECTION), so a click
|
|
158
|
+
* that only selected would be a click that did nothing at all.
|
|
159
|
+
*
|
|
160
|
+
* ⚠ Which is why the double click has to be swallowed: a real double click
|
|
161
|
+
* fires `click-card` twice, and two opens of a file are two viewer tabs. The
|
|
162
|
+
* guard is on the NODE, not on a timer alone — opening the same thing twice in
|
|
163
|
+
* a moment is the accident; opening two different things quickly is somebody
|
|
164
|
+
* working fast.
|
|
165
|
+
*/
|
|
166
|
+
const lastOpen = ref<{ path: string; at: number }>({ path: '', at: 0 });
|
|
167
|
+
function openNode(n: FileNode) {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
if (n.path === lastOpen.value.path && now - lastOpen.value.at < 700) return;
|
|
170
|
+
lastOpen.value = { path: n.path, at: now };
|
|
171
|
+
emit('open-node', n);
|
|
172
|
+
}
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<template>
|
|
176
|
+
<!-- ⚠ No title of its own, and as of 2026-09-13 nobody else draws one either.
|
|
177
|
+
The explorer used to render "Home" into the breadcrumb row above this;
|
|
178
|
+
the owner had it removed ("ver sayfa içinde salak bir Home yazısı var,
|
|
179
|
+
onu kaldıralım") because the panel row, the tab and the address bar
|
|
180
|
+
already say it three times over. The section headings below stay — they
|
|
181
|
+
name the blocks, not the page. -->
|
|
182
|
+
<div class="fe-home" data-testid="home-view">
|
|
183
|
+
<!-- ── Storages ──────────────────────────────────────────────────── -->
|
|
184
|
+
<section class="fe-home__section" data-testid="home-storages">
|
|
185
|
+
<h2 class="fe-home__heading">{{ t('sidenav.storages') }}</h2>
|
|
186
|
+
|
|
187
|
+
<div v-if="shownStorages.length" class="fe-home__storages">
|
|
188
|
+
<button
|
|
189
|
+
v-for="s in shownStorages"
|
|
190
|
+
:key="s.name"
|
|
191
|
+
type="button"
|
|
192
|
+
class="fe-home__storage"
|
|
193
|
+
data-testid="home-storage-card"
|
|
194
|
+
:data-storage="s.name"
|
|
195
|
+
:title="s.label || s.name"
|
|
196
|
+
@click="emit('open-storage', s.name)"
|
|
197
|
+
>
|
|
198
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
199
|
+
<span class="fe-home__storage-tile" aria-hidden="true" v-html="storageTile"></span>
|
|
200
|
+
<span class="fe-home__storage-main">
|
|
201
|
+
<span class="fe-home__storage-label">{{ s.label || s.name }}</span>
|
|
202
|
+
<span class="fe-home__storage-meta">{{ storageCaption(s) }}</span>
|
|
203
|
+
</span>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- ⚠ The filter hid them — NOT "no storages yet". Two different facts,
|
|
208
|
+
and borrowing the second sentence for the first tells an operator
|
|
209
|
+
their drives are gone. One row per block, `data-testid` included, so
|
|
210
|
+
a test can tell the two apart too. -->
|
|
211
|
+
<p
|
|
212
|
+
v-else-if="filteredEmpty(storages.length, shownStorages.length)"
|
|
213
|
+
class="fe-home__nomatch"
|
|
214
|
+
data-testid="home-storages-nomatch"
|
|
215
|
+
>
|
|
216
|
+
{{ t('filter.empty.title') }}
|
|
217
|
+
</p>
|
|
218
|
+
|
|
219
|
+
<div v-else class="fe-state fe-home__state" data-testid="home-storages-empty">
|
|
220
|
+
<svg
|
|
221
|
+
class="fe-state__art"
|
|
222
|
+
viewBox="0 0 120 100"
|
|
223
|
+
width="96"
|
|
224
|
+
height="80"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
stroke-width="2"
|
|
228
|
+
stroke-linecap="round"
|
|
229
|
+
stroke-linejoin="round"
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
>
|
|
232
|
+
<rect x="26" y="30" width="68" height="18" rx="5" />
|
|
233
|
+
<rect x="26" y="54" width="68" height="18" rx="5" />
|
|
234
|
+
<path d="M38 39h.01M38 63h.01" />
|
|
235
|
+
</svg>
|
|
236
|
+
<p class="fe-state__title">{{ t('home.storages.empty') }}</p>
|
|
237
|
+
<p class="fe-state__hint">{{ t('home.storages.hint') }}</p>
|
|
238
|
+
</div>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
<!-- ── Recent ────────────────────────────────────────────────────── -->
|
|
242
|
+
<section class="fe-home__section" data-testid="home-recent">
|
|
243
|
+
<h2 class="fe-home__heading">{{ t('sidenav.recent') }}</h2>
|
|
244
|
+
|
|
245
|
+
<GridView
|
|
246
|
+
v-if="shownRecent.length"
|
|
247
|
+
:files="shownRecent"
|
|
248
|
+
:selected="NO_SELECTION"
|
|
249
|
+
:locale="locale"
|
|
250
|
+
:thumb-src="thumbSrc"
|
|
251
|
+
@click-card="openNode"
|
|
252
|
+
@dbl-card="openNode"
|
|
253
|
+
@context-card="(n: FileNode, ev: MouseEvent) => emit('context-node', n, ev)"
|
|
254
|
+
/>
|
|
255
|
+
<!-- ⚠ A block still loading says so, and must NOT fall through to the
|
|
256
|
+
empty state: every visit would otherwise flash "Nothing starred yet"
|
|
257
|
+
at somebody who has starred things. -->
|
|
258
|
+
<p v-else-if="loading" class="fe-home__loading">{{ t('loading') }}</p>
|
|
259
|
+
<p
|
|
260
|
+
v-else-if="filteredEmpty(recent.length, shownRecent.length)"
|
|
261
|
+
class="fe-home__nomatch"
|
|
262
|
+
data-testid="home-recent-nomatch"
|
|
263
|
+
>
|
|
264
|
+
{{ t('filter.empty.title') }}
|
|
265
|
+
</p>
|
|
266
|
+
<div v-else class="fe-state fe-home__state" data-testid="home-recent-empty">
|
|
267
|
+
<svg
|
|
268
|
+
class="fe-state__art"
|
|
269
|
+
viewBox="0 0 120 100"
|
|
270
|
+
width="96"
|
|
271
|
+
height="80"
|
|
272
|
+
fill="none"
|
|
273
|
+
stroke="currentColor"
|
|
274
|
+
stroke-width="2"
|
|
275
|
+
stroke-linecap="round"
|
|
276
|
+
stroke-linejoin="round"
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
>
|
|
279
|
+
<circle cx="60" cy="50" r="28" />
|
|
280
|
+
<path d="M60 32v18l12 8" />
|
|
281
|
+
</svg>
|
|
282
|
+
<p class="fe-state__title">{{ t('empty.recent.title') }}</p>
|
|
283
|
+
<p class="fe-state__hint">{{ t('empty.recent.hint') }}</p>
|
|
284
|
+
</div>
|
|
285
|
+
</section>
|
|
286
|
+
|
|
287
|
+
<!-- ── Starred ───────────────────────────────────────────────────── -->
|
|
288
|
+
<section class="fe-home__section" data-testid="home-starred">
|
|
289
|
+
<h2 class="fe-home__heading">{{ t('sidenav.starred') }}</h2>
|
|
290
|
+
|
|
291
|
+
<GridView
|
|
292
|
+
v-if="shownStarred.length"
|
|
293
|
+
:files="shownStarred"
|
|
294
|
+
:selected="NO_SELECTION"
|
|
295
|
+
:locale="locale"
|
|
296
|
+
:thumb-src="thumbSrc"
|
|
297
|
+
@click-card="openNode"
|
|
298
|
+
@dbl-card="openNode"
|
|
299
|
+
@context-card="(n: FileNode, ev: MouseEvent) => emit('context-node', n, ev)"
|
|
300
|
+
/>
|
|
301
|
+
<p v-else-if="loading" class="fe-home__loading">{{ t('loading') }}</p>
|
|
302
|
+
<p
|
|
303
|
+
v-else-if="filteredEmpty(starred.length, shownStarred.length)"
|
|
304
|
+
class="fe-home__nomatch"
|
|
305
|
+
data-testid="home-starred-nomatch"
|
|
306
|
+
>
|
|
307
|
+
{{ t('filter.empty.title') }}
|
|
308
|
+
</p>
|
|
309
|
+
<div v-else class="fe-state fe-home__state" data-testid="home-starred-empty">
|
|
310
|
+
<svg
|
|
311
|
+
class="fe-state__art"
|
|
312
|
+
viewBox="0 0 120 100"
|
|
313
|
+
width="96"
|
|
314
|
+
height="80"
|
|
315
|
+
fill="none"
|
|
316
|
+
stroke="currentColor"
|
|
317
|
+
stroke-width="2"
|
|
318
|
+
stroke-linecap="round"
|
|
319
|
+
stroke-linejoin="round"
|
|
320
|
+
aria-hidden="true"
|
|
321
|
+
>
|
|
322
|
+
<path
|
|
323
|
+
d="M60 26l9 18.6 20.4 3-14.8 14.4 3.5 20.4L60 72.8 41.9 82.4l3.5-20.4L30.6 47.6l20.4-3z"
|
|
324
|
+
/>
|
|
325
|
+
</svg>
|
|
326
|
+
<p class="fe-state__title">{{ t('empty.starred.title') }}</p>
|
|
327
|
+
<p class="fe-state__hint">{{ t('empty.starred.hint') }}</p>
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
</div>
|
|
331
|
+
</template>
|