@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
|
@@ -30,7 +30,7 @@ const emit = defineEmits<{
|
|
|
30
30
|
}>();
|
|
31
31
|
|
|
32
32
|
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
33
|
-
const { t } = useLocale(locale);
|
|
33
|
+
const { t, formatDate } = useLocale(locale);
|
|
34
34
|
|
|
35
35
|
const {
|
|
36
36
|
exports,
|
|
@@ -156,10 +156,13 @@ async function copyLine() {
|
|
|
156
156
|
copyTimer = setTimeout(() => (copied.value = false), 1600);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/* zaman:z1 — one date formatter for the package. This was a bare
|
|
160
|
+
* `toLocaleDateString()`: the browser's locale and the browser's zone, so a
|
|
161
|
+
* key minted at 23:30 in Istanbul was dated a day early for a viewer reading
|
|
162
|
+
* UTC and came out in the wrong language besides. */
|
|
159
163
|
function shortDate(v?: string | null): string {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
|
164
|
+
const ms = new Date(v ?? '').getTime();
|
|
165
|
+
return Number.isNaN(ms) ? '' : formatDate(ms);
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
function scopeOf(e: NFSExport): string {
|
|
@@ -46,59 +46,107 @@ const hintCombos = computed(() => ({
|
|
|
46
46
|
|
|
47
47
|
// ------------------------------------------------------------------
|
|
48
48
|
// Step definitions. `target` returns the element to spotlight (null =
|
|
49
|
-
// centered card, e.g. the closing shortcuts step).
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
// centered card, e.g. the closing shortcuts step).
|
|
50
|
+
//
|
|
51
|
+
// gorunum:v3-shell — every target below is a `data-testid` or a class the
|
|
52
|
+
// shell owns, resolved from the live DOM. It used to find two of its
|
|
53
|
+
// buttons by matching their localized `title` attribute, which quietly
|
|
54
|
+
// stopped working the moment those verbs moved into a menu: `byTitle(r,
|
|
55
|
+
// t('toolbar.upload'))` matched nothing at 1440px, so the upload step was
|
|
56
|
+
// dropped from the walk and nobody could see that it had been. A step that
|
|
57
|
+
// disappears is indistinguishable from a step that was never written.
|
|
58
|
+
//
|
|
59
|
+
// ⚠ Two steps point at a control the narrow shell draws DIFFERENTLY — the
|
|
60
|
+
// panel (a column at 1440px, a closed drawer behind `toolbar-nav` at
|
|
61
|
+
// 390px) and the create button ("+ New" and its menu, versus the upload
|
|
62
|
+
// FAB). They say a different sentence there, and the sentence is chosen
|
|
63
|
+
// from THE ELEMENT THAT ACTUALLY RESOLVED, never from a width: one branch,
|
|
64
|
+
// no media query to keep in sync with the stylesheet.
|
|
52
65
|
// ------------------------------------------------------------------
|
|
53
66
|
|
|
54
|
-
function byTitle(root: HTMLElement, label: string): HTMLElement | null {
|
|
55
|
-
const nodes = root.querySelectorAll<HTMLElement>('button[title]');
|
|
56
|
-
for (const el of Array.from(nodes)) {
|
|
57
|
-
if (el.getAttribute('title') === label) return el;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
67
|
interface TourStep {
|
|
63
68
|
id: string;
|
|
64
69
|
titleKey: string;
|
|
65
|
-
|
|
70
|
+
/** A key, or a key chosen from whichever element resolved (see above). */
|
|
71
|
+
descKey: string | ((el: HTMLElement | null) => string);
|
|
66
72
|
target: (root: HTMLElement) => HTMLElement | null;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const STEPS: TourStep[] = [
|
|
70
76
|
{
|
|
77
|
+
// The navigation panel: the standard views, then STORAGES, then
|
|
78
|
+
// CONNECTIONS. At 390px it is a drawer that starts closed, so there is
|
|
79
|
+
// nothing to spotlight and the button that opens it is the target.
|
|
71
80
|
id: 'nav',
|
|
72
81
|
titleKey: 'tour.step.nav.title',
|
|
73
|
-
descKey:
|
|
74
|
-
|
|
82
|
+
descKey: (el) =>
|
|
83
|
+
el?.dataset.testid === 'sidenav' ? 'tour.step.nav.desc' : 'tour.step.nav.desc_closed',
|
|
84
|
+
target: (r) =>
|
|
85
|
+
r.querySelector<HTMLElement>('[data-testid="sidenav"]') ||
|
|
86
|
+
r.querySelector<HTMLElement>('[data-testid="toolbar-nav"]'),
|
|
75
87
|
},
|
|
76
88
|
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
// "+ New" — the menu that replaced the Upload / New folder pair. The
|
|
90
|
+
// narrow shell has no panel on screen and draws the upload FAB instead,
|
|
91
|
+
// which does ONE of the three things the menu offers, so it gets a
|
|
92
|
+
// sentence that promises only that one.
|
|
93
|
+
id: 'new',
|
|
94
|
+
titleKey: 'tour.step.new.title',
|
|
95
|
+
descKey: (el) =>
|
|
96
|
+
el?.dataset.testid === 'sidenav-new' ? 'tour.step.new.desc' : 'tour.step.new.desc_fab',
|
|
80
97
|
target: (r) =>
|
|
81
|
-
|
|
98
|
+
r.querySelector<HTMLElement>('[data-testid="sidenav-new"]') ||
|
|
99
|
+
r.querySelector<HTMLElement>('.fe-fab'),
|
|
82
100
|
},
|
|
83
101
|
{
|
|
102
|
+
// ⚠ The FIELD, not `.fe-search__input`. The wide header's input carries
|
|
103
|
+
// both class names and the narrow one carries only `fe-drivesearch__input`
|
|
104
|
+
// — so the old selector resolved at 1440px and matched nothing at 390px,
|
|
105
|
+
// where the field is the widest thing on the row. The testid is on the
|
|
106
|
+
// wrapper in both layouts, and spotlighting the wrapper also takes in the
|
|
107
|
+
// advanced-search and palette buttons the copy now names.
|
|
84
108
|
id: 'search',
|
|
85
109
|
titleKey: 'tour.step.search.title',
|
|
86
110
|
descKey: 'tour.step.search.desc',
|
|
87
|
-
target: (r) =>
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
target: (r) => r.querySelector<HTMLElement>('[data-testid="drive-search"]'),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
// The breadcrumb — this was the old first step's target while its title
|
|
115
|
+
// said "Storages & folders", which is the panel. Now it has its own step
|
|
116
|
+
// and its own title. Absent on Home, which has no address; the step drops
|
|
117
|
+
// itself there rather than pointing at the heading that takes its place.
|
|
118
|
+
id: 'crumb',
|
|
119
|
+
titleKey: 'tour.step.crumb.title',
|
|
120
|
+
descKey: 'tour.step.crumb.desc',
|
|
121
|
+
target: (r) => r.querySelector<HTMLElement>('.fe-breadcrumb'),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
// The filter row. Unconditional in the shell now, but only drawn where
|
|
125
|
+
// there is a listing to filter — not at the multi-storage virtual root,
|
|
126
|
+
// which is where a first run usually starts, so this step is often the
|
|
127
|
+
// one that drops itself.
|
|
128
|
+
id: 'filters',
|
|
129
|
+
titleKey: 'tour.step.filters.title',
|
|
130
|
+
descKey: 'tour.step.filters.desc',
|
|
131
|
+
target: (r) => r.querySelector<HTMLElement>('[data-testid="filterbar"]'),
|
|
90
132
|
},
|
|
91
133
|
{
|
|
92
134
|
id: 'view',
|
|
93
135
|
titleKey: 'tour.step.view.title',
|
|
94
136
|
descKey: 'tour.step.view.desc',
|
|
137
|
+
// Still `.fe-toolbar__view`: ViewSwitcher kept its class when it moved
|
|
138
|
+
// from the header to the breadcrumb row.
|
|
95
139
|
target: (r) => r.querySelector<HTMLElement>('.fe-toolbar__view'),
|
|
96
140
|
},
|
|
97
141
|
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
// Sharing used to spotlight `.fe__body` — the whole listing, which is to
|
|
143
|
+
// say nothing in particular — while talking about a context menu. The
|
|
144
|
+
// details panel is where a share link is actually minted, so the step
|
|
145
|
+
// points at the control that opens it and names the context menu too.
|
|
146
|
+
id: 'details',
|
|
147
|
+
titleKey: 'tour.step.details.title',
|
|
148
|
+
descKey: 'tour.step.details.desc',
|
|
149
|
+
target: (r) => r.querySelector<HTMLElement>('[data-testid="subhead-inspector"]'),
|
|
102
150
|
},
|
|
103
151
|
{
|
|
104
152
|
id: 'help',
|
|
@@ -143,6 +191,24 @@ interface Rect {
|
|
|
143
191
|
const spot = ref<Rect | null>(null);
|
|
144
192
|
const cardStyle = ref<Record<string, string>>({});
|
|
145
193
|
const cardEl = ref<HTMLElement | null>(null);
|
|
194
|
+
/**
|
|
195
|
+
* The element the CURRENT step resolved to, kept so a step whose control
|
|
196
|
+
* differs by width can pick its sentence from what is really there.
|
|
197
|
+
*
|
|
198
|
+
* Written at the top of `place()`, which runs before the card re-renders
|
|
199
|
+
* (the `stepIdx` watcher flushes 'pre') and again on every resize — so the
|
|
200
|
+
* copy follows the spotlight instead of lagging a step behind it. Assigning
|
|
201
|
+
* the same element twice is not a change, so a scroll storm costs nothing.
|
|
202
|
+
*/
|
|
203
|
+
const targetEl = ref<HTMLElement | null>(null);
|
|
204
|
+
|
|
205
|
+
/** The step's sentence, with the shortcut names filled in. */
|
|
206
|
+
const stepDesc = computed(() => {
|
|
207
|
+
const s = step.value;
|
|
208
|
+
if (!s) return '';
|
|
209
|
+
const key = typeof s.descKey === 'function' ? s.descKey(targetEl.value) : s.descKey;
|
|
210
|
+
return t(key, hintCombos.value);
|
|
211
|
+
});
|
|
146
212
|
|
|
147
213
|
const PAD = 6; // spotlight breathing room around the target
|
|
148
214
|
const GAP = 12; // gap between spotlight and the card
|
|
@@ -152,6 +218,7 @@ async function place() {
|
|
|
152
218
|
const r = props.root;
|
|
153
219
|
if (!s) return;
|
|
154
220
|
const el = r ? s.target(r) : null;
|
|
221
|
+
targetEl.value = el;
|
|
155
222
|
if (!el) {
|
|
156
223
|
spot.value = null; // centered card
|
|
157
224
|
cardStyle.value = {};
|
|
@@ -169,17 +236,50 @@ async function place() {
|
|
|
169
236
|
width: rect.width + PAD * 2,
|
|
170
237
|
height: rect.height + PAD * 2,
|
|
171
238
|
};
|
|
172
|
-
// Card: below the spotlight
|
|
173
|
-
//
|
|
239
|
+
// Card: below the spotlight, then above, then beside it.
|
|
240
|
+
//
|
|
241
|
+
// gorunum:v3-shell — the SIDE branches are why this is four cases and not
|
|
242
|
+
// two. The navigation panel is a full-height column, so neither "below" nor
|
|
243
|
+
// "above" fits and the old code clamped the card to the top of the viewport
|
|
244
|
+
// — which put it flat on top of the panel, hiding the very rows ("Home, My
|
|
245
|
+
// files, Shared with me…") the step was reading out. A coach mark that
|
|
246
|
+
// covers its own subject is worse than no coach mark: it looks like the
|
|
247
|
+
// product drew a dialog in the wrong place.
|
|
174
248
|
await nextTick();
|
|
175
249
|
const vw = window.innerWidth;
|
|
176
250
|
const vh = window.innerHeight;
|
|
177
251
|
const cw = Math.min(340, vw - 24);
|
|
178
252
|
const ch = cardEl.value?.offsetHeight ?? 160;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
253
|
+
const sTop = rect.top - PAD;
|
|
254
|
+
const sBottom = rect.bottom + PAD;
|
|
255
|
+
const sLeft = rect.left - PAD;
|
|
256
|
+
const sRight = rect.right + PAD;
|
|
257
|
+
// Centred on the target along the other axis, then clamped into view.
|
|
258
|
+
const clampX = (x: number) => Math.min(Math.max(12, x), Math.max(12, vw - cw - 12));
|
|
259
|
+
const clampY = (y: number) => Math.min(Math.max(12, y), Math.max(12, vh - ch - 12));
|
|
260
|
+
const midX = clampX(rect.left + rect.width / 2 - cw / 2);
|
|
261
|
+
const midY = clampY(rect.top + rect.height / 2 - ch / 2);
|
|
262
|
+
|
|
263
|
+
let top: number;
|
|
264
|
+
let left: number;
|
|
265
|
+
if (sBottom + GAP + ch <= vh - 12) {
|
|
266
|
+
top = sBottom + GAP;
|
|
267
|
+
left = midX;
|
|
268
|
+
} else if (sTop - GAP - ch >= 12) {
|
|
269
|
+
top = sTop - GAP - ch;
|
|
270
|
+
left = midX;
|
|
271
|
+
} else if (sRight + GAP + cw <= vw - 12) {
|
|
272
|
+
left = sRight + GAP;
|
|
273
|
+
top = midY;
|
|
274
|
+
} else if (sLeft - GAP - cw >= 12) {
|
|
275
|
+
left = sLeft - GAP - cw;
|
|
276
|
+
top = midY;
|
|
277
|
+
} else {
|
|
278
|
+
// Nothing fits — a target that fills the viewport. Clamp, and accept the
|
|
279
|
+
// overlap rather than drawing the card off screen.
|
|
280
|
+
top = clampY(sBottom + GAP);
|
|
281
|
+
left = midX;
|
|
282
|
+
}
|
|
183
283
|
cardStyle.value = {
|
|
184
284
|
top: `${Math.round(top)}px`,
|
|
185
285
|
left: `${Math.round(left)}px`,
|
|
@@ -187,8 +287,30 @@ async function place() {
|
|
|
187
287
|
};
|
|
188
288
|
}
|
|
189
289
|
|
|
290
|
+
let placeRaf = 0;
|
|
291
|
+
/**
|
|
292
|
+
* Re-place after a resize or a scroll — but on the NEXT frame, never inline.
|
|
293
|
+
*
|
|
294
|
+
* ⚠ The explorer decides it is narrow from a ResizeObserver on its own root
|
|
295
|
+
* (FileExplorer.vue, `isNarrow`), and ResizeObserver callbacks run AFTER the
|
|
296
|
+
* frame's rAF callbacks — so neither this `window` listener nor a single rAF
|
|
297
|
+
* has seen the new layout yet. Measured: shrinking 1440 → 390 with the tour
|
|
298
|
+
* open left the spotlight on the navigation panel (the element was still in
|
|
299
|
+
* the DOM when this handler asked for it) both inline and after one frame;
|
|
300
|
+
* only a SECOND resize moved it onto the top bar's button. Two frames plus a
|
|
301
|
+
* tick puts this after the observer has fired and Vue has patched, so the
|
|
302
|
+
* first resize gets the right answer — and the step's sentence, which is
|
|
303
|
+
* chosen from that same element, changes with it.
|
|
304
|
+
*/
|
|
190
305
|
function onViewportChange() {
|
|
191
|
-
if (props.open)
|
|
306
|
+
if (!props.open) return;
|
|
307
|
+
if (placeRaf) cancelAnimationFrame(placeRaf);
|
|
308
|
+
placeRaf = requestAnimationFrame(() => {
|
|
309
|
+
placeRaf = requestAnimationFrame(() => {
|
|
310
|
+
placeRaf = 0;
|
|
311
|
+
void nextTick().then(place);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
192
314
|
}
|
|
193
315
|
|
|
194
316
|
// ------------------------------------------------------------------
|
|
@@ -250,6 +372,7 @@ onMounted(() => {
|
|
|
250
372
|
onBeforeUnmount(() => {
|
|
251
373
|
window.removeEventListener('resize', onViewportChange);
|
|
252
374
|
window.removeEventListener('scroll', onViewportChange, true);
|
|
375
|
+
if (placeRaf) cancelAnimationFrame(placeRaf);
|
|
253
376
|
});
|
|
254
377
|
|
|
255
378
|
// Theme cascade outside `.fe` — same pattern as ContextMenu.
|
|
@@ -309,7 +432,7 @@ const themeClass = computed(() => `fe-ctx-backdrop--theme-${props.theme || 'auto
|
|
|
309
432
|
{{ t('tour.progress', { n: stepIdx + 1, m: total }) }}
|
|
310
433
|
</p>
|
|
311
434
|
<h3 class="fe-tour__title">{{ t(step.titleKey) }}</h3>
|
|
312
|
-
<p class="fe-tour__desc">{{
|
|
435
|
+
<p class="fe-tour__desc">{{ stepDesc }}</p>
|
|
313
436
|
<div class="fe-tour__dots" aria-hidden="true">
|
|
314
437
|
<span
|
|
315
438
|
v-for="(s, i) in activeSteps"
|
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* mode), so every viewer the modal knows (image/video/pdf/office/code/
|
|
7
7
|
* 3D/archive/…) works in quick-look for free. What this layer adds:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
9
|
+
* - the quick-look key closes the peek again (Space by default — macOS
|
|
10
|
+
* Quick Look convention), and the open key promotes it into the full
|
|
11
|
+
* open flow (Enter by default, emit 'open-full'). Both come from the
|
|
12
|
+
* shortcut registry rather than from a comparison against the default
|
|
13
|
+
* key, so a remap reaches the overlay too
|
|
10
14
|
* - ← ↑ / → ↓ ask the host to move the selection; the host keeps the
|
|
11
|
-
* `file` prop in sync so the preview follows the selection
|
|
12
|
-
*
|
|
13
|
-
* - a small floating hint bar
|
|
15
|
+
* `file` prop in sync so the preview follows the selection. These are
|
|
16
|
+
* the peek's own arrows, not registry actions
|
|
17
|
+
* - a small floating hint bar naming those keys, read from the registry
|
|
14
18
|
*
|
|
15
19
|
* Keys are handled in window CAPTURE phase with stopPropagation so the
|
|
16
20
|
* global shortcut registry never double-handles them; events that
|
|
@@ -39,14 +43,31 @@ const props = defineProps<{
|
|
|
39
43
|
pdfWorkerUrl?: string | null;
|
|
40
44
|
viewerBaseUrl?: string | null;
|
|
41
45
|
theme?: 'light' | 'dark' | 'auto';
|
|
46
|
+
/* === gorunum:v1-viewer — passed straight through to PreviewModal ===
|
|
47
|
+
* The peek knows nothing about the listing either; the host that answers
|
|
48
|
+
* `nav` is the one that can say "1 of 9". */
|
|
49
|
+
/** 1-based position of `file` in the host's file list. */
|
|
50
|
+
index?: number;
|
|
51
|
+
/** How many files that list holds. */
|
|
52
|
+
total?: number;
|
|
53
|
+
/** API origin for the star toggle. */
|
|
54
|
+
apiBase?: string;
|
|
55
|
+
/** Draw the share action (the host must listen to `@share`). */
|
|
56
|
+
shareEnabled?: boolean;
|
|
42
57
|
}>();
|
|
43
58
|
|
|
44
59
|
const emit = defineEmits<{
|
|
45
60
|
(e: 'close'): void;
|
|
46
|
-
/** Arrow navigation: -1 = previous file, +1 = next file.
|
|
61
|
+
/** Arrow navigation: -1 = previous file, +1 = next file. Now raised by the
|
|
62
|
+
* overlay's edge chevrons as well as by the arrow keys — ONE contract, two
|
|
63
|
+
* triggers, because a second navigation path would drift from this one. */
|
|
47
64
|
(e: 'nav', delta: number): void;
|
|
48
65
|
/** Enter — close the peek and open the file for real. */
|
|
49
66
|
(e: 'open-full'): void;
|
|
67
|
+
/** The viewer's share action (only reachable when `shareEnabled`). */
|
|
68
|
+
(e: 'share'): void;
|
|
69
|
+
/** The viewer's star toggle succeeded. */
|
|
70
|
+
(e: 'starred', value: boolean): void;
|
|
50
71
|
}>();
|
|
51
72
|
|
|
52
73
|
const { t } = useLocale(() => props.locale);
|
|
@@ -152,7 +173,15 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
|
|
|
152
173
|
:drawio-url="drawioUrl"
|
|
153
174
|
:pdf-worker-url="pdfWorkerUrl"
|
|
154
175
|
:viewer-base-url="viewerBaseUrl"
|
|
176
|
+
:index="index"
|
|
177
|
+
:total="total"
|
|
178
|
+
:api-base="apiBase"
|
|
179
|
+
:share-enabled="shareEnabled"
|
|
180
|
+
:nav-enabled="true"
|
|
155
181
|
@close="emit('close')"
|
|
182
|
+
@nav="(d: number) => emit('nav', d)"
|
|
183
|
+
@share="emit('share')"
|
|
184
|
+
@starred="(v: boolean) => emit('starred', v)"
|
|
156
185
|
/>
|
|
157
186
|
<!--
|
|
158
187
|
Teleported under <body> for the same reason as ContextMenu and
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* files. Backed by `GET /api/files/manager/recent?limit=20`.
|
|
5
5
|
*/
|
|
6
6
|
import { ref, onMounted, watch } from 'vue';
|
|
7
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
7
8
|
|
|
8
9
|
interface RecentNode {
|
|
9
10
|
id: number;
|
|
@@ -83,8 +84,19 @@ watch(() => props.refreshKey, load);
|
|
|
83
84
|
<div class="filex-recent">
|
|
84
85
|
<header>
|
|
85
86
|
<h3>Recently opened</h3>
|
|
86
|
-
<button
|
|
87
|
-
|
|
87
|
+
<button
|
|
88
|
+
class="filex-recent-refresh"
|
|
89
|
+
type="button"
|
|
90
|
+
title="Refresh"
|
|
91
|
+
aria-label="Refresh"
|
|
92
|
+
@click="load"
|
|
93
|
+
:disabled="loading"
|
|
94
|
+
>
|
|
95
|
+
<!-- ikon:emoji — ↻ was a bare arrow glyph with no label at all, so
|
|
96
|
+
the only thing naming this button was its shape. Same mark as the
|
|
97
|
+
toolbar's Refresh now, and it says what it is. -->
|
|
98
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
99
|
+
<span aria-hidden="true" v-html="actionIconSvg('refresh')"></span>
|
|
88
100
|
</button>
|
|
89
101
|
</header>
|
|
90
102
|
|
|
@@ -130,6 +142,9 @@ watch(() => props.refreshKey, load);
|
|
|
130
142
|
color: var(--fe-text, var(--filex-text, #111827));
|
|
131
143
|
}
|
|
132
144
|
.filex-recent-refresh {
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
133
148
|
background: transparent;
|
|
134
149
|
border: none;
|
|
135
150
|
color: var(--fe-text-muted, var(--filex-text-muted, #9ca3af));
|
|
@@ -36,7 +36,7 @@ const emit = defineEmits<{
|
|
|
36
36
|
}>();
|
|
37
37
|
|
|
38
38
|
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
39
|
-
const { t } = useLocale(locale);
|
|
39
|
+
const { t, formatDate } = useLocale(locale);
|
|
40
40
|
|
|
41
41
|
const {
|
|
42
42
|
keys,
|
|
@@ -150,10 +150,13 @@ async function copy(text: string, id: string) {
|
|
|
150
150
|
copyTimer = setTimeout(() => (copied.value = null), 1600);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* zaman:z1 — one date formatter for the package. This was a bare
|
|
154
|
+
* `toLocaleDateString()`: the browser's locale and the browser's zone, so a
|
|
155
|
+
* key minted at 23:30 in Istanbul was dated a day early for a viewer reading
|
|
156
|
+
* UTC and came out in the wrong language besides. */
|
|
153
157
|
function shortDate(v?: string | null): string {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
|
158
|
+
const ms = new Date(v ?? '').getTime();
|
|
159
|
+
return Number.isNaN(ms) ? '' : formatDate(ms);
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
/** What a key is limited to, in one line. */
|
|
@@ -49,7 +49,7 @@ const emit = defineEmits<{
|
|
|
49
49
|
}>();
|
|
50
50
|
|
|
51
51
|
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
52
|
-
const { t } = useLocale(locale);
|
|
52
|
+
const { t, formatDate } = useLocale(locale);
|
|
53
53
|
|
|
54
54
|
const { keys, connection, loading, error, canAdd, hasUsableKey, load, add, setDisabled, remove } =
|
|
55
55
|
useSSHKeys(props.config);
|
|
@@ -119,10 +119,13 @@ async function drop(k: SSHPublicKey) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* zaman:z1 — one date formatter for the package. This was a bare
|
|
123
|
+
* `toLocaleDateString()`: the browser's locale and the browser's zone, so a
|
|
124
|
+
* key minted at 23:30 in Istanbul was dated a day early for a viewer reading
|
|
125
|
+
* UTC and came out in the wrong language besides. */
|
|
122
126
|
function shortDate(v?: string | null): string {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
|
127
|
+
const ms = new Date(v ?? '').getTime();
|
|
128
|
+
return Number.isNaN(ms) ? '' : formatDate(ms);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
/** Shown the way OpenSSH prints it, so it can be compared with `ssh-keygen -lf`. */
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
21
21
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
22
22
|
import { useLocale } from '../composables/useLocale';
|
|
23
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
23
24
|
import {
|
|
24
25
|
comboFromEvent,
|
|
25
26
|
effectiveCombo,
|
|
26
27
|
findShortcutConflict,
|
|
28
|
+
isReservedCombo,
|
|
27
29
|
resetAllShortcuts,
|
|
28
30
|
resetShortcut,
|
|
29
31
|
setShortcutOverride,
|
|
@@ -66,6 +68,8 @@ const anyOverridden = computed(() => list.value.some((s) => s.overridden));
|
|
|
66
68
|
|
|
67
69
|
const capturingId = ref<string | null>(null);
|
|
68
70
|
const conflict = ref<{ id: string; combo: string; conflictId: string; fixed: boolean } | null>(null);
|
|
71
|
+
/* tus:t1 — the user pressed something the browser keeps for itself. */
|
|
72
|
+
const reserved = ref<{ id: string; combo: string } | null>(null);
|
|
69
73
|
|
|
70
74
|
function labelOf(id: string): string {
|
|
71
75
|
const def = SHORTCUT_ACTIONS.find((a) => a.id === id);
|
|
@@ -74,6 +78,7 @@ function labelOf(id: string): string {
|
|
|
74
78
|
|
|
75
79
|
function beginCapture(id: string) {
|
|
76
80
|
conflict.value = null;
|
|
81
|
+
reserved.value = null;
|
|
77
82
|
capturingId.value = id;
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -84,6 +89,13 @@ function cancelCapture() {
|
|
|
84
89
|
function attemptAssign(id: string, combo: string) {
|
|
85
90
|
capturingId.value = null;
|
|
86
91
|
if (combo === effectiveCombo(id)) return; // unchanged
|
|
92
|
+
/* tus:t1 — a combo the browser takes before the page sees it would be a key
|
|
93
|
+
* that does nothing where the user is standing. Refuse it and say why,
|
|
94
|
+
* rather than storing a binding that never fires. */
|
|
95
|
+
if (isReservedCombo(combo)) {
|
|
96
|
+
reserved.value = { id, combo };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
87
99
|
const clash = findShortcutConflict(combo, id);
|
|
88
100
|
if (clash) {
|
|
89
101
|
conflict.value = { id, combo, conflictId: clash.id, fixed: clash.fixed };
|
|
@@ -104,13 +116,19 @@ function dismissConflict() {
|
|
|
104
116
|
conflict.value = null;
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
function dismissReserved() {
|
|
120
|
+
reserved.value = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
function onReset(id: string) {
|
|
108
124
|
conflict.value = null;
|
|
125
|
+
reserved.value = null;
|
|
109
126
|
resetShortcut(id);
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
function onResetAll() {
|
|
113
130
|
conflict.value = null;
|
|
131
|
+
reserved.value = null;
|
|
114
132
|
capturingId.value = null;
|
|
115
133
|
resetAllShortcuts();
|
|
116
134
|
}
|
|
@@ -145,6 +163,7 @@ watch(
|
|
|
145
163
|
window.removeEventListener('keydown', onWindowKeydown, true);
|
|
146
164
|
capturingId.value = null;
|
|
147
165
|
conflict.value = null;
|
|
166
|
+
reserved.value = null;
|
|
148
167
|
}
|
|
149
168
|
},
|
|
150
169
|
);
|
|
@@ -171,7 +190,9 @@ function onClose() {
|
|
|
171
190
|
:disabled="!anyOverridden"
|
|
172
191
|
@click="onResetAll"
|
|
173
192
|
>
|
|
174
|
-
|
|
193
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
194
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('restore')"></span>
|
|
195
|
+
{{ t('shortcuts.settings.reset_all') }}
|
|
175
196
|
</button>
|
|
176
197
|
</div>
|
|
177
198
|
|
|
@@ -201,6 +222,14 @@ function onClose() {
|
|
|
201
222
|
</span>
|
|
202
223
|
<template v-for="(combo, i) in s.keys" v-else :key="combo">
|
|
203
224
|
<kbd class="fe-kbd">{{ combo }}</kbd>
|
|
225
|
+
<!-- tus:t1 — this combo never reaches a browser tab. Say so
|
|
226
|
+
on the row rather than letting the user discover it by
|
|
227
|
+
pressing the key and watching nothing happen. -->
|
|
228
|
+
<span
|
|
229
|
+
v-if="isReservedCombo(combo)"
|
|
230
|
+
class="fe-shortset__reserved"
|
|
231
|
+
:title="t('shortcuts.settings.reserved_title')"
|
|
232
|
+
>{{ t('shortcuts.settings.reserved_badge') }}</span>
|
|
204
233
|
<span v-if="i < s.keys.length - 1" class="fe-shortcuts__or" aria-hidden="true">/</span>
|
|
205
234
|
</template>
|
|
206
235
|
</template>
|
|
@@ -231,7 +260,8 @@ function onClose() {
|
|
|
231
260
|
:aria-label="t('shortcuts.settings.reset')"
|
|
232
261
|
@click="onReset(s.id)"
|
|
233
262
|
>
|
|
234
|
-
|
|
263
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
264
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('restore')"></span>
|
|
235
265
|
</button>
|
|
236
266
|
</template>
|
|
237
267
|
<span v-else class="fe-shortset__fixed">{{ t('shortcuts.settings.fixed') }}</span>
|
|
@@ -260,6 +290,17 @@ function onClose() {
|
|
|
260
290
|
</button>
|
|
261
291
|
</span>
|
|
262
292
|
</div>
|
|
293
|
+
<div v-if="reserved && reserved.id === s.id" class="fe-shortset__conflict" role="alert">
|
|
294
|
+
<span class="fe-shortset__conflict-msg">
|
|
295
|
+
<kbd class="fe-kbd">{{ reserved.combo }}</kbd>
|
|
296
|
+
{{ t('shortcuts.settings.reserved') }}
|
|
297
|
+
</span>
|
|
298
|
+
<span class="fe-shortset__conflict-actions">
|
|
299
|
+
<button type="button" class="fe-btn fe-shortset__btn" @click="dismissReserved">
|
|
300
|
+
{{ t('shortcuts.settings.conflict_cancel') }}
|
|
301
|
+
</button>
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
263
304
|
</div>
|
|
264
305
|
</section>
|
|
265
306
|
</div>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { computed } from 'vue';
|
|
12
12
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
13
13
|
import { useLocale } from '../composables/useLocale';
|
|
14
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
15
|
import { useShortcutList, type ShortcutView } from '../composables/useKeyboardShortcuts';
|
|
15
16
|
import Modal from '../modals/Modal.vue';
|
|
16
17
|
|
|
@@ -72,7 +73,9 @@ const groups = computed(() => {
|
|
|
72
73
|
</div>
|
|
73
74
|
<template #actions>
|
|
74
75
|
<button type="button" class="fe-btn fe-btn--primary" @click="emit('customize')">
|
|
75
|
-
|
|
76
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
77
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('shortcut-settings')"></span>
|
|
78
|
+
{{ t('shortcuts.customize') }}
|
|
76
79
|
</button>
|
|
77
80
|
<button type="button" class="fe-btn" @click="emit('close')">{{ t('viewer.close') }}</button>
|
|
78
81
|
</template>
|