@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.
- package/README.md +32 -25
- package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
- package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
- package/dist/CsvViewer-CyuwENI_.js +141 -0
- package/dist/CsvViewer-CyuwENI_.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
- package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
- package/dist/EpubViewer-K9PjCMmS.js +163 -0
- package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
- package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
- package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
- package/dist/MermaidViewer-Bp73aId8.js +137 -0
- package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
- package/dist/PsdViewer-C2eLivw1.js +122 -0
- package/dist/PsdViewer-C2eLivw1.js.map +1 -0
- package/dist/TiffViewer-DsHg5gcz.js +142 -0
- package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
- package/dist/Viewer3D-C_dc2HN9.js +75 -0
- package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
- package/dist/filex-core.js +218 -20361
- 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-vxMZpYb-.js +26700 -0
- package/dist/index-vxMZpYb-.js.map +1 -0
- package/dist/index.d.ts +4212 -862
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2154 -506
- 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 +39 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +951 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +172 -56
- package/src/components/GridView.vue +331 -82
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1173 -194
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +26 -1
- 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 +6 -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 +135 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +35 -14
- package/src/components/Toolbar.vue +953 -465
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +89 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useLocale.ts +382 -26
- package/src/composables/useRowTouch.ts +95 -0
- package/src/index.ts +177 -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 +31 -2
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +151 -116
- package/src/lib/timezone.ts +501 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +416 -26
- package/src/locales/tr.ts +409 -26
- 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 +557 -476
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5409 -8
- package/src/styles/variables.css +139 -65
- 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
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* decide whether to extract or download.
|
|
11
11
|
*/
|
|
12
12
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
13
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
13
15
|
|
|
14
16
|
interface ArchiveEntry {
|
|
15
17
|
name: string;
|
|
@@ -22,7 +24,7 @@ const props = defineProps<{
|
|
|
22
24
|
url: string;
|
|
23
25
|
filePath?: string;
|
|
24
26
|
ext: string;
|
|
25
|
-
t?: (key: string) => string;
|
|
27
|
+
t?: (key: string, vars?: Record<string, string | number>) => string;
|
|
26
28
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
27
29
|
authCredentials?: RequestCredentials;
|
|
28
30
|
/** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
|
|
@@ -37,8 +39,11 @@ const entries = ref<ArchiveEntry[]>([]);
|
|
|
37
39
|
const loading = ref(true);
|
|
38
40
|
const error = ref<string | null>(null);
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what
|
|
43
|
+
* picks the singular (`viewer.archive.entries_one`) from the count, and it can
|
|
44
|
+
* only do that if it is told the count. Same shape CsvViewer's helper has. */
|
|
45
|
+
function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
|
|
46
|
+
return props.t ? props.t(key, vars) : fallback;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
function fmtSize(n: number): string {
|
|
@@ -87,25 +92,54 @@ watch(() => props.filePath, load);
|
|
|
87
92
|
|
|
88
93
|
const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
|
|
89
94
|
const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
95
|
+
|
|
96
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
97
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
98
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
99
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
100
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
101
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
102
|
+
* still picture can say "still going". */
|
|
103
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
104
|
+
|
|
105
|
+
/** A row inside the archive, drawn the way the listing draws the same kind. */
|
|
106
|
+
function entryTile(e: ArchiveEntry): string {
|
|
107
|
+
const name = e.name || '';
|
|
108
|
+
const dot = name.lastIndexOf('.');
|
|
109
|
+
return fileIconTile({
|
|
110
|
+
type: e.is_dir ? 'dir' : 'file',
|
|
111
|
+
extension: dot > 0 ? name.slice(dot + 1) : '',
|
|
112
|
+
basename: name,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
90
115
|
</script>
|
|
91
116
|
|
|
92
117
|
<template>
|
|
93
118
|
<div class="filex-viewer-archive">
|
|
94
119
|
<div v-if="error" class="filex-viewer-fallback">
|
|
95
|
-
|
|
120
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
121
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
96
122
|
<p>{{ error }}</p>
|
|
97
123
|
</div>
|
|
98
124
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
99
|
-
|
|
125
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
126
|
+
<span
|
|
127
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
v-html="actionIconSvg('progress')"
|
|
130
|
+
></span>
|
|
100
131
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
101
132
|
</div>
|
|
102
133
|
<div v-else-if="entries.length === 0" class="filex-viewer-fallback">
|
|
103
|
-
|
|
134
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
135
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
104
136
|
<p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>
|
|
105
137
|
</div>
|
|
106
138
|
<div v-else class="filex-viewer-archive__pane">
|
|
107
139
|
<div class="filex-viewer-archive__summary">
|
|
108
|
-
|
|
140
|
+
<!-- ⚠ The count is IN the message ("{n} files"). A `{{ fileCount }}`
|
|
141
|
+
printed in front of it read "3 3 files". -->
|
|
142
|
+
{{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}
|
|
109
143
|
· {{ fmtSize(totalSize) }}
|
|
110
144
|
</div>
|
|
111
145
|
<table class="filex-viewer-archive__table">
|
|
@@ -118,7 +152,11 @@ const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
|
118
152
|
<tbody>
|
|
119
153
|
<tr v-for="(e, i) in entries" :key="i" :data-dir="e.is_dir ? '1' : '0'">
|
|
120
154
|
<td>
|
|
121
|
-
|
|
155
|
+
<!-- ikon:emoji — the same tile the listing draws for the same
|
|
156
|
+
kind of file, so an archive's contents and the folder it
|
|
157
|
+
came from read as one product. -->
|
|
158
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
159
|
+
<span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(e)"></span>
|
|
122
160
|
{{ e.name }}
|
|
123
161
|
</td>
|
|
124
162
|
<td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - tab vs comma auto-detection (manual override via toolbar)
|
|
15
15
|
*/
|
|
16
16
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
18
20
|
|
|
19
21
|
const props = defineProps<{
|
|
@@ -147,17 +149,32 @@ watch(filter, () => {
|
|
|
147
149
|
function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
|
|
148
150
|
return props.t ? props.t(key, vars) : fallback;
|
|
149
151
|
}
|
|
152
|
+
|
|
153
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
154
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
155
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
156
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
157
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
158
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
159
|
+
* still picture can say "still going". */
|
|
160
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
150
161
|
</script>
|
|
151
162
|
|
|
152
163
|
<template>
|
|
153
164
|
<div class="filex-viewer-csv">
|
|
154
165
|
<div class="filex-viewer-csv__pane">
|
|
155
166
|
<div v-if="error" class="filex-viewer-fallback">
|
|
156
|
-
|
|
167
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
168
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
157
169
|
<p>{{ error }}</p>
|
|
158
170
|
</div>
|
|
159
171
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
160
|
-
|
|
172
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
173
|
+
<span
|
|
174
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
175
|
+
aria-hidden="true"
|
|
176
|
+
v-html="actionIconSvg('progress')"
|
|
177
|
+
></span>
|
|
161
178
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
162
179
|
</div>
|
|
163
180
|
<table v-else class="filex-viewer-csv__table">
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* (drawio uses the same shape: POST `{path, content}` body).
|
|
16
16
|
*/
|
|
17
17
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
18
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
19
20
|
|
|
20
21
|
const props = defineProps<{
|
|
@@ -180,12 +181,22 @@ onBeforeUnmount(() => {
|
|
|
180
181
|
function tt(key: string, fallback: string): string {
|
|
181
182
|
return props.t ? props.t(key) : fallback;
|
|
182
183
|
}
|
|
184
|
+
|
|
185
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
186
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
187
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
188
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
189
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
190
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
191
|
+
* still picture can say "still going". */
|
|
192
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
183
193
|
</script>
|
|
184
194
|
|
|
185
195
|
<template>
|
|
186
196
|
<div class="filex-viewer-drawio">
|
|
187
197
|
<div v-if="error" class="filex-viewer-fallback">
|
|
188
|
-
|
|
198
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
199
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
189
200
|
<p>{{ error }}</p>
|
|
190
201
|
</div>
|
|
191
202
|
<iframe
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - font size +/- (Themes API)
|
|
14
14
|
* - graceful fallback if peer not installed
|
|
15
15
|
*/
|
|
16
|
-
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
16
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
18
|
|
|
18
19
|
const props = defineProps<{
|
|
19
20
|
url: string;
|
|
@@ -42,6 +43,29 @@ const loading = ref(true);
|
|
|
42
43
|
let book: any = null;
|
|
43
44
|
let rendition: any = null;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How long an epub.js step may hang before we call it a failure.
|
|
48
|
+
*
|
|
49
|
+
* Generous on purpose: a large publication on a slow disk legitimately takes
|
|
50
|
+
* seconds, and a false "cannot open" on a book that would have rendered is a
|
|
51
|
+
* worse bug than the one this guards against.
|
|
52
|
+
*/
|
|
53
|
+
const EPUB_STEP_TIMEOUT_MS = 15000;
|
|
54
|
+
|
|
55
|
+
/** Reject if `p` has not settled within `EPUB_STEP_TIMEOUT_MS`. */
|
|
56
|
+
function withTimeout<T>(p: Promise<T>, label: string): Promise<T> {
|
|
57
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
58
|
+
return Promise.race([
|
|
59
|
+
p,
|
|
60
|
+
new Promise<never>((_, reject) => {
|
|
61
|
+
timer = setTimeout(
|
|
62
|
+
() => reject(new Error(`EPUB ${label} timed out after ${EPUB_STEP_TIMEOUT_MS}ms`)),
|
|
63
|
+
EPUB_STEP_TIMEOUT_MS,
|
|
64
|
+
);
|
|
65
|
+
}),
|
|
66
|
+
]).finally(() => clearTimeout(timer)) as Promise<T>;
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
async function load(): Promise<void> {
|
|
46
70
|
loading.value = true;
|
|
47
71
|
ready.value = false;
|
|
@@ -81,14 +105,24 @@ async function load(): Promise<void> {
|
|
|
81
105
|
flow: 'paginated',
|
|
82
106
|
manager: 'default',
|
|
83
107
|
});
|
|
84
|
-
|
|
108
|
+
// ⚠⚠ Both of these have to be raced against a clock. epub.js resolves
|
|
109
|
+
// `display()` and `loaded.navigation` from its own internal promises, and
|
|
110
|
+
// when the archive is not a readable EPUB it simply NEVER settles —
|
|
111
|
+
// neither resolve nor reject — so the `catch` below can't fire and
|
|
112
|
+
// `finally` never runs. Measured 2026-09-13 on an 8-byte `handbook.epub`:
|
|
113
|
+
// the pane sat on "Yükleniyor…" forever with no error anywhere.
|
|
114
|
+
await withTimeout(rendition.display(), 'display');
|
|
85
115
|
rendition.themes.fontSize(fontSize.value + '%');
|
|
86
|
-
|
|
116
|
+
// `<any>` explicitly: `book` is `any`, so without it `T` infers as `{}`
|
|
117
|
+
// and `nav.toc` stops type-checking.
|
|
118
|
+
const nav = await withTimeout<any>(book.loaded.navigation, 'navigation');
|
|
87
119
|
toc.value = (nav?.toc ?? []) as TocNode[];
|
|
88
120
|
ready.value = true;
|
|
89
121
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
// The reader gets the localized sentence; epub.js's own wording (and the
|
|
123
|
+
// timeout marker) goes to the console for whoever is debugging.
|
|
124
|
+
console.warn('[filex] EPUB load failed:', err);
|
|
125
|
+
error.value = tt('viewer.failed_to_load', 'Failed to load file');
|
|
92
126
|
} finally {
|
|
93
127
|
loading.value = false;
|
|
94
128
|
}
|
|
@@ -165,12 +199,22 @@ watch(
|
|
|
165
199
|
function tt(key: string, fallback: string): string {
|
|
166
200
|
return props.t ? props.t(key) : fallback;
|
|
167
201
|
}
|
|
202
|
+
|
|
203
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
204
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
205
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
206
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
207
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
208
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
209
|
+
* still picture can say "still going". */
|
|
210
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
168
211
|
</script>
|
|
169
212
|
|
|
170
213
|
<template>
|
|
171
214
|
<div ref="root" class="filex-viewer-epub">
|
|
172
215
|
<div v-if="error" class="filex-viewer-fallback">
|
|
173
|
-
|
|
216
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
217
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
174
218
|
<p>{{ error }}</p>
|
|
175
219
|
</div>
|
|
176
220
|
<template v-else>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* notebook doesn't surprise the user.
|
|
15
15
|
*/
|
|
16
16
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
18
20
|
import { ensureHighlight } from '../composables/useMonacoLoader';
|
|
19
21
|
|
|
@@ -192,17 +194,32 @@ const stats = computed(() => {
|
|
|
192
194
|
function tt(key: string, fallback: string): string {
|
|
193
195
|
return props.t ? props.t(key) : fallback;
|
|
194
196
|
}
|
|
197
|
+
|
|
198
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
199
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
200
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
201
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
202
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
203
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
204
|
+
* still picture can say "still going". */
|
|
205
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
195
206
|
</script>
|
|
196
207
|
|
|
197
208
|
<template>
|
|
198
209
|
<div class="filex-viewer-ipynb">
|
|
199
210
|
<div class="filex-viewer-ipynb__pane">
|
|
200
211
|
<div v-if="error" class="filex-viewer-fallback">
|
|
201
|
-
|
|
212
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
213
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
202
214
|
<p>{{ error }}</p>
|
|
203
215
|
</div>
|
|
204
216
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
205
|
-
|
|
217
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
218
|
+
<span
|
|
219
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
v-html="actionIconSvg('progress')"
|
|
222
|
+
></span>
|
|
206
223
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
207
224
|
</div>
|
|
208
225
|
<div v-else>
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* SVG node is inserted into a wrapper that supports pan + scale via
|
|
9
9
|
* pure CSS transforms (no extra panzoom dep).
|
|
10
10
|
*/
|
|
11
|
-
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
11
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
12
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
13
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
12
14
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
13
15
|
|
|
14
16
|
const props = defineProps<{
|
|
@@ -142,6 +144,15 @@ onBeforeUnmount(() => {
|
|
|
142
144
|
function tt(key: string, fallback: string): string {
|
|
143
145
|
return props.t ? props.t(key) : fallback;
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
149
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
150
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
151
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
152
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
153
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
154
|
+
* still picture can say "still going". */
|
|
155
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
145
156
|
</script>
|
|
146
157
|
|
|
147
158
|
<template>
|
|
@@ -158,14 +169,20 @@ function tt(key: string, fallback: string): string {
|
|
|
158
169
|
v-if="error"
|
|
159
170
|
class="filex-viewer-fallback"
|
|
160
171
|
>
|
|
161
|
-
|
|
172
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
173
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
162
174
|
<p>{{ error }}</p>
|
|
163
175
|
</div>
|
|
164
176
|
<div
|
|
165
177
|
v-else-if="loading"
|
|
166
178
|
class="filex-viewer-fallback"
|
|
167
179
|
>
|
|
168
|
-
|
|
180
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
181
|
+
<span
|
|
182
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
v-html="actionIconSvg('progress')"
|
|
185
|
+
></span>
|
|
169
186
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
170
187
|
</div>
|
|
171
188
|
<div
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* installed (the parent PreviewModal handles that branch).
|
|
21
21
|
*/
|
|
22
22
|
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
|
|
23
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
24
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
23
25
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
24
26
|
|
|
25
27
|
const props = defineProps<{
|
|
@@ -366,6 +368,15 @@ watch(() => props.url, load);
|
|
|
366
368
|
function tt(key: string, fallback: string): string {
|
|
367
369
|
return props.t ? props.t(key) : fallback;
|
|
368
370
|
}
|
|
371
|
+
|
|
372
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
373
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
374
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
375
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
376
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
377
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
378
|
+
* still picture can say "still going". */
|
|
379
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
369
380
|
</script>
|
|
370
381
|
|
|
371
382
|
<template>
|
|
@@ -410,11 +421,14 @@ function tt(key: string, fallback: string): string {
|
|
|
410
421
|
:disabled="saveStatus === 'saving'"
|
|
411
422
|
@click="saveAnnotations"
|
|
412
423
|
>
|
|
424
|
+
<!-- ikon:emoji — the button said ⏳ / ✓, an emoji and a dingbat, in
|
|
425
|
+
a row of word buttons. Words are what this control has always
|
|
426
|
+
been; the two transient states get theirs. -->
|
|
413
427
|
{{
|
|
414
428
|
saveStatus === 'saving'
|
|
415
|
-
? '
|
|
429
|
+
? tt('viewer.saving', 'Saving…')
|
|
416
430
|
: saveStatus === 'saved'
|
|
417
|
-
? '
|
|
431
|
+
? tt('viewer.saved', 'Saved')
|
|
418
432
|
: tt('viewer.save_annotations', 'Save')
|
|
419
433
|
}}
|
|
420
434
|
</button>
|
|
@@ -429,11 +443,17 @@ function tt(key: string, fallback: string): string {
|
|
|
429
443
|
class="filex-viewer-pdf__embed"
|
|
430
444
|
/>
|
|
431
445
|
<div v-else-if="error" class="filex-viewer-fallback">
|
|
432
|
-
|
|
446
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
447
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
433
448
|
<p>{{ error }}</p>
|
|
434
449
|
</div>
|
|
435
450
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
436
|
-
|
|
451
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
452
|
+
<span
|
|
453
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
454
|
+
aria-hidden="true"
|
|
455
|
+
v-html="actionIconSvg('progress')"
|
|
456
|
+
></span>
|
|
437
457
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
438
458
|
</div>
|
|
439
459
|
<div ref="containerEl" v-show="!loading && !error" class="filex-viewer-pdf__pages" />
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* — left as TODO for V2 since `ag-psd` doesn't ship a composite
|
|
10
10
|
* pipeline of its own).
|
|
11
11
|
*/
|
|
12
|
-
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
12
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
13
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
13
15
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
14
16
|
|
|
15
17
|
const props = defineProps<{
|
|
@@ -108,7 +110,14 @@ async function load(): Promise<void> {
|
|
|
108
110
|
flattenLayers(psd, flat);
|
|
109
111
|
layers.value = flat;
|
|
110
112
|
} catch (err) {
|
|
111
|
-
|
|
113
|
+
// ⚠ The person gets the localized sentence; the decoder's own words go to
|
|
114
|
+
// the console for whoever is debugging. `ag-psd` throws raw internals
|
|
115
|
+
// ("Offset is outside the bounds of the DataView" on a truncated file),
|
|
116
|
+
// and printing that verbatim put an untranslated English stack message in
|
|
117
|
+
// the middle of a Turkish UI — measured 2026-09-13 on a 6-byte
|
|
118
|
+
// `mockup.psd`.
|
|
119
|
+
console.warn('[filex] PSD decode failed:', err);
|
|
120
|
+
error.value = tt('viewer.failed_to_load', 'Failed to load file');
|
|
112
121
|
} finally {
|
|
113
122
|
loading.value = false;
|
|
114
123
|
}
|
|
@@ -133,17 +142,32 @@ watch(() => props.url, load);
|
|
|
133
142
|
function tt(key: string, fallback: string): string {
|
|
134
143
|
return props.t ? props.t(key) : fallback;
|
|
135
144
|
}
|
|
145
|
+
|
|
146
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
147
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
148
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
149
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
150
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
151
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
152
|
+
* still picture can say "still going". */
|
|
153
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
136
154
|
</script>
|
|
137
155
|
|
|
138
156
|
<template>
|
|
139
157
|
<div class="filex-viewer-psd">
|
|
140
158
|
<div class="filex-viewer-psd__pane">
|
|
141
159
|
<div v-if="error" class="filex-viewer-fallback">
|
|
142
|
-
|
|
160
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
161
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
143
162
|
<p>{{ error }}</p>
|
|
144
163
|
</div>
|
|
145
164
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
146
|
-
|
|
165
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
166
|
+
<span
|
|
167
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
v-html="actionIconSvg('progress')"
|
|
170
|
+
></span>
|
|
147
171
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
148
172
|
</div>
|
|
149
173
|
<canvas
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* conversion.
|
|
10
10
|
*/
|
|
11
11
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
12
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
13
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
12
14
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
13
15
|
|
|
14
16
|
const props = defineProps<{
|
|
@@ -143,17 +145,32 @@ const pageLabel = computed(() => {
|
|
|
143
145
|
function tt(key: string, fallback: string): string {
|
|
144
146
|
return props.t ? props.t(key) : fallback;
|
|
145
147
|
}
|
|
148
|
+
|
|
149
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
150
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
151
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
152
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
153
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
154
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
155
|
+
* still picture can say "still going". */
|
|
156
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
146
157
|
</script>
|
|
147
158
|
|
|
148
159
|
<template>
|
|
149
160
|
<div class="filex-viewer-tiff">
|
|
150
161
|
<div class="filex-viewer-tiff__pane">
|
|
151
162
|
<div v-if="error" class="filex-viewer-fallback">
|
|
152
|
-
|
|
163
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
164
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
153
165
|
<p>{{ error }}</p>
|
|
154
166
|
</div>
|
|
155
167
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
156
|
-
|
|
168
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
169
|
+
<span
|
|
170
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
v-html="actionIconSvg('progress')"
|
|
173
|
+
></span>
|
|
157
174
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
158
175
|
</div>
|
|
159
176
|
<canvas
|
package/src/viewers/Viewer3D.vue
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* (sweep-2026-05-09 bug 21.)
|
|
20
20
|
*/
|
|
21
21
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
22
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
23
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
22
24
|
|
|
23
25
|
const props = defineProps<{
|
|
24
26
|
url: string;
|
|
@@ -57,6 +59,22 @@ async function load(): Promise<void> {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* model-viewer failed to FETCH or PARSE the model.
|
|
64
|
+
*
|
|
65
|
+
* ⚠ `ready` only means the ~80 KB web component imported. The model itself is
|
|
66
|
+
* fetched and decoded by model-viewer AFTER it mounts, and when that throws
|
|
67
|
+
* (a truncated or non-glTF file) it renders an empty canvas and reports the
|
|
68
|
+
* failure only on this event. Without the listener the person got a silent
|
|
69
|
+
* black rectangle and nothing else — measured 2026-09-13 on an 8-byte
|
|
70
|
+
* `turbine.glb`, where the console had `RangeError: Invalid DataView length 12`
|
|
71
|
+
* and the pane had no text at all.
|
|
72
|
+
*/
|
|
73
|
+
function onModelError(): void {
|
|
74
|
+
ready.value = false;
|
|
75
|
+
error.value = props.t ? props.t('viewer.failed_to_load') : 'Failed to load file';
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
onMounted(load);
|
|
61
79
|
|
|
62
80
|
watch(() => props.url, () => {
|
|
@@ -64,6 +82,15 @@ watch(() => props.url, () => {
|
|
|
64
82
|
// fallback message).
|
|
65
83
|
load();
|
|
66
84
|
});
|
|
85
|
+
|
|
86
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
87
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
88
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
89
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
90
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
91
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
92
|
+
* still picture can say "still going". */
|
|
93
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
67
94
|
</script>
|
|
68
95
|
|
|
69
96
|
<template>
|
|
@@ -77,13 +104,20 @@ watch(() => props.url, () => {
|
|
|
77
104
|
shadow-intensity="1"
|
|
78
105
|
:alt="ext + ' model'"
|
|
79
106
|
style="width: 100%; height: 100%; min-height: 480px; display: block"
|
|
107
|
+
@error="onModelError"
|
|
80
108
|
/>
|
|
81
109
|
<div v-else-if="error" class="filex-viewer-fallback">
|
|
82
|
-
|
|
110
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
111
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
83
112
|
<p>{{ error }}</p>
|
|
84
113
|
</div>
|
|
85
114
|
<div v-else class="filex-viewer-fallback">
|
|
86
|
-
|
|
115
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
116
|
+
<span
|
|
117
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
v-html="actionIconSvg('progress')"
|
|
120
|
+
></span>
|
|
87
121
|
<p>{{ t ? t('viewer.loading') : 'Loading…' }}</p>
|
|
88
122
|
</div>
|
|
89
123
|
</div>
|