@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
|
@@ -25,9 +25,12 @@ import type { Component } from 'vue';
|
|
|
25
25
|
import type { FileNode } from '../types/FileNode';
|
|
26
26
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
27
27
|
import Modal from './Modal.vue';
|
|
28
|
+
import StarButton from '../components/StarButton.vue';
|
|
28
29
|
import { ensureMonaco, getMonaco, ensureHighlight } from '../composables/useMonacoLoader';
|
|
29
30
|
import { useLocale } from '../composables/useLocale';
|
|
30
31
|
import { browserProbeURL } from '../lib/externalReach';
|
|
32
|
+
import { fileIconTile } from '../lib/fileIcons';
|
|
33
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
31
34
|
|
|
32
35
|
const props = defineProps<{
|
|
33
36
|
open: boolean;
|
|
@@ -68,13 +71,40 @@ const props = defineProps<{
|
|
|
68
71
|
* vs-dark). When unset Monaco falls back to `prefers-color-scheme`
|
|
69
72
|
* and the modal cascade follows whatever `.fe` parent it gets. */
|
|
70
73
|
theme?: 'light' | 'dark' | 'auto';
|
|
74
|
+
/* === gorunum:v1-viewer — the full-bleed overlay's own contract ===
|
|
75
|
+
* Everything below is OPTIONAL and answered by the host, because the
|
|
76
|
+
* modal cannot invent any of it: it is handed ONE file and has no idea
|
|
77
|
+
* what list that file came out of, nor where the API lives. */
|
|
78
|
+
/** 1-based position of `file` in the host's current file list. */
|
|
79
|
+
index?: number;
|
|
80
|
+
/** How many files that list holds. `index` + `total` draw "1 of 9";
|
|
81
|
+
* with either missing the counter is simply absent — a counter that
|
|
82
|
+
* guesses is worse than no counter. */
|
|
83
|
+
total?: number;
|
|
84
|
+
/** Draw the prev/next chevrons even when index/total are unknown.
|
|
85
|
+
* QuickLook sets it because its host already answers `nav`. */
|
|
86
|
+
navEnabled?: boolean;
|
|
87
|
+
/** API origin for the star toggle — same meaning as ExplorerConfig.apiBase.
|
|
88
|
+
* Omitted = same origin, which is what the admin UI wants. */
|
|
89
|
+
apiBase?: string;
|
|
90
|
+
/** Draw the share action. Off by default on purpose: a share button with
|
|
91
|
+
* no host listening to `@share` is a control that lies. */
|
|
92
|
+
shareEnabled?: boolean;
|
|
71
93
|
}>();
|
|
72
94
|
|
|
73
95
|
const emit = defineEmits<{
|
|
74
96
|
(e: 'close'): void;
|
|
97
|
+
/** Previous / next file. −1 and +1, the SAME contract QuickLook already
|
|
98
|
+
* emits upward — the chevrons are a second trigger for it, not a second
|
|
99
|
+
* navigation mechanism. The host keeps the `file` prop in sync. */
|
|
100
|
+
(e: 'nav', delta: number): void;
|
|
101
|
+
/** The host opens its own share dialog. Only reachable when `shareEnabled`. */
|
|
102
|
+
(e: 'share'): void;
|
|
103
|
+
/** The star toggle succeeded — so the host can update its listing row. */
|
|
104
|
+
(e: 'starred', value: boolean): void;
|
|
75
105
|
}>();
|
|
76
106
|
|
|
77
|
-
const { t } = useLocale(() => props.locale);
|
|
107
|
+
const { t, formatSize, formatDate, nodeDisplayName } = useLocale(() => props.locale);
|
|
78
108
|
|
|
79
109
|
function ext(f: FileNode | null): string {
|
|
80
110
|
return (f?.extension || '').toLowerCase();
|
|
@@ -298,6 +328,148 @@ function stripAdapter(p: string): string {
|
|
|
298
328
|
return idx === -1 ? p : p.slice(idx + 3);
|
|
299
329
|
}
|
|
300
330
|
|
|
331
|
+
/* === gorunum:v1-viewer — full-bleed overlay chrome ===
|
|
332
|
+
*
|
|
333
|
+
* The card modal (title bar + Download/Close footer) is gone; what replaces
|
|
334
|
+
* it is one bar across the top, a chevron on each screen edge and a floating
|
|
335
|
+
* zoom pill. `chromeless` is untouched: the standalone /files/edit route still
|
|
336
|
+
* gets the bare viewer with no chrome at all, which is the whole point of that
|
|
337
|
+
* flag — the browser tab is its container.
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
/** Chrome is drawn for the in-page overlay only. */
|
|
341
|
+
const showChrome = computed(() => !props.chromeless);
|
|
342
|
+
|
|
343
|
+
const viewerEl = ref<HTMLElement | null>(null);
|
|
344
|
+
|
|
345
|
+
const tileHtml = computed(() => (props.file ? fileIconTile(props.file) : ''));
|
|
346
|
+
const displayName = computed(() => (props.file ? nodeDisplayName(props.file) : ''));
|
|
347
|
+
|
|
348
|
+
/** "1 of 9" — only when the host answered BOTH halves. */
|
|
349
|
+
const counterText = computed(() => {
|
|
350
|
+
const i = props.index;
|
|
351
|
+
const n = props.total;
|
|
352
|
+
if (!i || !n || n < 1 || i < 1) return '';
|
|
353
|
+
return t('viewer.counter', { i, n });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/** `246.3 KB • Sep 9, 2026 • 1 of 9`, with any unknown part left out
|
|
357
|
+
* rather than printed as a placeholder. */
|
|
358
|
+
const metaLine = computed(() => {
|
|
359
|
+
const f = props.file;
|
|
360
|
+
if (!f) return '';
|
|
361
|
+
const parts: string[] = [];
|
|
362
|
+
if (typeof f.size === 'number' && f.type === 'file') parts.push(formatSize(f.size));
|
|
363
|
+
const when = formatDate(f.last_modified);
|
|
364
|
+
if (when) parts.push(when);
|
|
365
|
+
if (counterText.value) parts.push(counterText.value);
|
|
366
|
+
return parts.join(' • ');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/** The chevrons appear only where prev/next actually goes somewhere. */
|
|
370
|
+
const canNav = computed(
|
|
371
|
+
() => showChrome.value && (props.navEnabled === true || (props.total ?? 0) > 1),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
/** Starring is the real thing, not a second implementation: StarButton +
|
|
375
|
+
* lib/star.ts, the same pair the listing rows use. It needs the DB node id,
|
|
376
|
+
* which client-synthesized rows do not carry — no id, no button. */
|
|
377
|
+
const starNodeId = computed(() =>
|
|
378
|
+
typeof props.file?.id === 'number' ? (props.file.id as number) : null,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Which kinds get the stage stretched instead of centred. Editors and
|
|
383
|
+
* document surfaces want every pixel; a photo wants to sit in the middle
|
|
384
|
+
* of the ground with room around it.
|
|
385
|
+
*/
|
|
386
|
+
const FILL_KINDS = new Set(['pdf', 'markdown', 'code', 'text', 'office', 'viewer']);
|
|
387
|
+
const stageModifier = computed(() =>
|
|
388
|
+
FILL_KINDS.has(kind.value) ? 'fe-preview--fill' : 'fe-preview--center',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
/* --- Zoom (image only) ----------------------------------------------
|
|
392
|
+
*
|
|
393
|
+
* ⚠ The pill is hidden for pdf / office / drawio. Those three paint their
|
|
394
|
+
* OWN zoom control inside their surface, and a second one wired to nothing
|
|
395
|
+
* is a control that lies — worse than no control, because the user reads a
|
|
396
|
+
* percentage that the thing on screen does not obey.
|
|
397
|
+
*/
|
|
398
|
+
const ZOOM_STEPS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
|
|
399
|
+
const zoom = ref(1);
|
|
400
|
+
const imgEl = ref<HTMLImageElement | null>(null);
|
|
401
|
+
/** The width the image settles at when it is fit to the stage (zoom = 1).
|
|
402
|
+
* Everything else is a multiple of it, which is what makes "100%" mean
|
|
403
|
+
* "the size you first saw" rather than "the pixel size of the file". */
|
|
404
|
+
const fitWidth = ref(0);
|
|
405
|
+
|
|
406
|
+
const canZoom = computed(() => showChrome.value && kind.value === 'image');
|
|
407
|
+
|
|
408
|
+
function measureFit(): void {
|
|
409
|
+
if (zoom.value !== 1 || !imgEl.value) return;
|
|
410
|
+
const w = imgEl.value.getBoundingClientRect().width;
|
|
411
|
+
if (w > 0) fitWidth.value = w;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const imageStyle = computed(() => {
|
|
415
|
+
if (zoom.value === 1 || !fitWidth.value) return undefined;
|
|
416
|
+
return {
|
|
417
|
+
width: `${Math.round(fitWidth.value * zoom.value)}px`,
|
|
418
|
+
height: 'auto',
|
|
419
|
+
maxWidth: 'none',
|
|
420
|
+
maxHeight: 'none',
|
|
421
|
+
} as Record<string, string>;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
function stepZoom(dir: 1 | -1): void {
|
|
425
|
+
const cur = zoom.value;
|
|
426
|
+
if (dir > 0) {
|
|
427
|
+
zoom.value = ZOOM_STEPS.find((s) => s > cur + 1e-6) ?? ZOOM_STEPS[ZOOM_STEPS.length - 1];
|
|
428
|
+
} else {
|
|
429
|
+
const lower = ZOOM_STEPS.filter((s) => s < cur - 1e-6);
|
|
430
|
+
zoom.value = lower.length ? lower[lower.length - 1] : ZOOM_STEPS[0];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function resetZoom(): void {
|
|
435
|
+
zoom.value = 1;
|
|
436
|
+
void nextTick(measureFit);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const zoomLabel = computed(() => `${Math.round(zoom.value * 100)}%`);
|
|
440
|
+
const canZoomIn = computed(() => zoom.value < ZOOM_STEPS[ZOOM_STEPS.length - 1] - 1e-6);
|
|
441
|
+
const canZoomOut = computed(() => zoom.value > ZOOM_STEPS[0] + 1e-6);
|
|
442
|
+
|
|
443
|
+
/* --- Fullscreen ------------------------------------------------------ */
|
|
444
|
+
const isFullscreen = ref(false);
|
|
445
|
+
|
|
446
|
+
function onFullscreenChange(): void {
|
|
447
|
+
isFullscreen.value = !!document.fullscreenElement;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function toggleFullscreen(): void {
|
|
451
|
+
try {
|
|
452
|
+
if (document.fullscreenElement) {
|
|
453
|
+
void document.exitFullscreen?.();
|
|
454
|
+
} else {
|
|
455
|
+
void viewerEl.value?.requestFullscreen?.();
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
/* a browser that refuses fullscreen just leaves the overlay as it is */
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The overlay has no backdrop ring to click — the card IS the viewport — so
|
|
464
|
+
* the ground around the content takes that job. `.self` is what keeps it
|
|
465
|
+
* honest: it only fires when the click landed on the ground itself, never on
|
|
466
|
+
* an editor, an iframe or the image.
|
|
467
|
+
*/
|
|
468
|
+
function onGroundClick(): void {
|
|
469
|
+
if (!showChrome.value) return;
|
|
470
|
+
emit('close');
|
|
471
|
+
}
|
|
472
|
+
|
|
301
473
|
const loading = ref(false);
|
|
302
474
|
const fetchError = ref<string | null>(null);
|
|
303
475
|
const rawText = ref<string>('');
|
|
@@ -713,6 +885,10 @@ async function runOrchestration(open: boolean, url: string, k: string): Promise<
|
|
|
713
885
|
viewerCmp.value = null;
|
|
714
886
|
viewerLoadError.value = null;
|
|
715
887
|
pdfFallbackToNative.value = false;
|
|
888
|
+
/* gorunum:v1-viewer — a new file starts at 100%. Carrying 400% over from
|
|
889
|
+
* the previous photo means the next one opens mid-crop with no hint why. */
|
|
890
|
+
zoom.value = 1;
|
|
891
|
+
fitWidth.value = 0;
|
|
716
892
|
disposeOnlyOfficeEditor();
|
|
717
893
|
disposeMonaco();
|
|
718
894
|
if (!open || !url) return;
|
|
@@ -772,6 +948,7 @@ watch(
|
|
|
772
948
|
// `officeEditor`/Monaco state below. Doing this in onMounted+nextTick
|
|
773
949
|
// guarantees every `let`/`function` in the file has been hoisted.
|
|
774
950
|
onMounted(() => {
|
|
951
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
775
952
|
void nextTick(() => {
|
|
776
953
|
if (!props.open) return;
|
|
777
954
|
void runOrchestration(props.open, src.value, kind.value);
|
|
@@ -851,6 +1028,7 @@ function disposeOnlyOfficeEditor(): void {
|
|
|
851
1028
|
}
|
|
852
1029
|
|
|
853
1030
|
onBeforeUnmount(() => {
|
|
1031
|
+
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
854
1032
|
disposeOnlyOfficeEditor();
|
|
855
1033
|
disposeMonaco();
|
|
856
1034
|
});
|
|
@@ -909,171 +1087,353 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
|
|
|
909
1087
|
</script>
|
|
910
1088
|
|
|
911
1089
|
<template>
|
|
912
|
-
<Modal
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
<div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
|
|
935
|
-
<div v-else-if="tooLarge" class="fe-preview__fallback">
|
|
936
|
-
{{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
|
|
1090
|
+
<Modal
|
|
1091
|
+
:open="open"
|
|
1092
|
+
size="xl"
|
|
1093
|
+
:title="file?.basename || ''"
|
|
1094
|
+
:chromeless="chromeless"
|
|
1095
|
+
:fullbleed="!chromeless"
|
|
1096
|
+
:theme="theme"
|
|
1097
|
+
@close="emit('close')"
|
|
1098
|
+
>
|
|
1099
|
+
<!-- === gorunum:v1-viewer ===
|
|
1100
|
+
A full-bleed overlay, not a card: one bar across the top, a chevron on
|
|
1101
|
+
each screen edge, a floating zoom pill at the bottom. `chromeless`
|
|
1102
|
+
renders the viewer BARE (no bar, no chevrons, no pill) because the
|
|
1103
|
+
standalone /files/edit route's container is the browser tab itself. -->
|
|
1104
|
+
<div ref="viewerEl" class="fe-viewer" :class="{ 'fe-viewer--bare': chromeless }">
|
|
1105
|
+
<header v-if="showChrome && file" class="fe-viewer__bar">
|
|
1106
|
+
<div class="fe-viewer__ident">
|
|
1107
|
+
<span class="fe-viewer__tile" aria-hidden="true" v-html="tileHtml"></span>
|
|
1108
|
+
<span class="fe-viewer__idcol">
|
|
1109
|
+
<span class="fe-viewer__name" :title="file.path">{{ displayName }}</span>
|
|
1110
|
+
<span v-if="metaLine" class="fe-viewer__meta">{{ metaLine }}</span>
|
|
1111
|
+
</span>
|
|
937
1112
|
</div>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1113
|
+
|
|
1114
|
+
<div class="fe-viewer__acts">
|
|
1115
|
+
<a
|
|
1116
|
+
:href="download"
|
|
1117
|
+
class="fe-viewer__act"
|
|
1118
|
+
:title="t('viewer.download')"
|
|
1119
|
+
:aria-label="t('viewer.download')"
|
|
1120
|
+
><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('download')"></span></a>
|
|
1121
|
+
<button
|
|
1122
|
+
v-if="shareEnabled"
|
|
1123
|
+
type="button"
|
|
1124
|
+
class="fe-viewer__act"
|
|
1125
|
+
:title="t('viewer.share')"
|
|
1126
|
+
:aria-label="t('viewer.share')"
|
|
1127
|
+
@click="emit('share')"
|
|
1128
|
+
><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('access')"></span></button>
|
|
1129
|
+
<!-- The listing's StarButton, not a copy of it: same component, same
|
|
1130
|
+
lib/star.ts request, same optimistic rollback. -->
|
|
1131
|
+
<StarButton
|
|
1132
|
+
v-if="starNodeId !== null"
|
|
1133
|
+
class="fe-viewer__act fe-viewer__act--star"
|
|
1134
|
+
:starred="file.starred === true"
|
|
1135
|
+
:node-id="starNodeId"
|
|
1136
|
+
:api-base="apiBase"
|
|
1137
|
+
:auth-headers="authHeaders"
|
|
1138
|
+
:auth-credentials="authCredentials"
|
|
1139
|
+
:locale="locale"
|
|
1140
|
+
compact
|
|
1141
|
+
@change="(v: boolean) => emit('starred', v)"
|
|
1142
|
+
/>
|
|
1143
|
+
<button
|
|
1144
|
+
v-if="openMode === 'view' && canEditKind && newTabEnabled !== false /* wiring:e2 */"
|
|
1145
|
+
type="button"
|
|
1146
|
+
class="fe-viewer__act"
|
|
1147
|
+
:title="t('viewer.edit')"
|
|
1148
|
+
:aria-label="t('viewer.edit')"
|
|
1149
|
+
@click="openEditInNewTab"
|
|
1150
|
+
><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('rename')"></span></button>
|
|
1151
|
+
<button
|
|
1152
|
+
v-if="newTabEnabled !== false /* wiring:e2 */"
|
|
1153
|
+
type="button"
|
|
1154
|
+
class="fe-viewer__act"
|
|
1155
|
+
:title="t('viewer.open_in_new_tab')"
|
|
1156
|
+
:aria-label="t('viewer.open_in_new_tab')"
|
|
1157
|
+
@click="openInNewTab"
|
|
1158
|
+
><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('open-tab')"></span></button>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
1161
|
+
<div class="fe-viewer__tail">
|
|
1162
|
+
<button
|
|
1163
|
+
type="button"
|
|
1164
|
+
class="fe-viewer__act fe-viewer__close"
|
|
1165
|
+
:title="t('viewer.close')"
|
|
1166
|
+
:aria-label="t('viewer.close')"
|
|
1167
|
+
@click="emit('close')"
|
|
1168
|
+
><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('close')"></span></button>
|
|
1169
|
+
</div>
|
|
1170
|
+
</header>
|
|
1171
|
+
|
|
1172
|
+
<div class="fe-viewer__stage" @click.self="onGroundClick">
|
|
1173
|
+
<div v-if="file" class="fe-preview" :class="stageModifier" @click.self="onGroundClick">
|
|
1174
|
+
<template v-if="kind === 'image'">
|
|
1175
|
+
<img
|
|
1176
|
+
ref="imgEl"
|
|
1177
|
+
:src="src"
|
|
1178
|
+
:alt="file.basename"
|
|
1179
|
+
class="fe-preview__image"
|
|
1180
|
+
:style="imageStyle"
|
|
1181
|
+
@load="measureFit"
|
|
963
1182
|
/>
|
|
1183
|
+
</template>
|
|
1184
|
+
<template v-else-if="kind === 'video'">
|
|
1185
|
+
<video :src="src" controls preload="metadata" class="fe-preview__video" />
|
|
1186
|
+
</template>
|
|
1187
|
+
<template v-else-if="kind === 'audio'">
|
|
1188
|
+
<audio :src="src" controls class="fe-preview__audio" />
|
|
1189
|
+
</template>
|
|
1190
|
+
<template v-else-if="kind === 'pdf'">
|
|
1191
|
+
<object :data="src" type="application/pdf" class="fe-preview__iframe">
|
|
1192
|
+
<div class="fe-preview__fallback">
|
|
1193
|
+
<!-- ikon:emoji — the file's own tile, the one the row behind
|
|
1194
|
+
this overlay is wearing. -->
|
|
1195
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
1196
|
+
<span class="fe-preview__fallback-icon" aria-hidden="true" v-html="tileHtml"></span>
|
|
1197
|
+
<p>{{ t('viewer.pdf_inline_failed') }}</p>
|
|
1198
|
+
<a :href="download" class="fe-btn fe-btn--primary" target="_blank" rel="noopener">{{ t('viewer.open_in_new_tab') }}</a>
|
|
1199
|
+
</div>
|
|
1200
|
+
</object>
|
|
1201
|
+
</template>
|
|
1202
|
+
|
|
1203
|
+
<template v-else-if="kind === 'markdown'">
|
|
1204
|
+
<div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
|
|
1205
|
+
<div v-else-if="tooLarge" class="fe-preview__fallback">
|
|
1206
|
+
{{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
|
|
1207
|
+
</div>
|
|
964
1208
|
<div
|
|
965
|
-
v-if="
|
|
966
|
-
class="fe-preview__md-split
|
|
967
|
-
>
|
|
968
|
-
|
|
1209
|
+
v-else-if="openMode === 'edit' && saveTextEndpoint"
|
|
1210
|
+
class="fe-preview__md-split"
|
|
1211
|
+
>
|
|
1212
|
+
<div class="fe-preview__md-split-bar">
|
|
1213
|
+
<span class="fe-preview__md-split-label">MARKDOWN</span>
|
|
1214
|
+
<span v-if="fetchError" class="fe-preview__md-split-error">{{ fetchError }}</span>
|
|
1215
|
+
<button
|
|
1216
|
+
type="button"
|
|
1217
|
+
class="fe-btn fe-btn--primary"
|
|
1218
|
+
:disabled="!mdDirty || mdSaving"
|
|
1219
|
+
@click="saveMarkdown"
|
|
1220
|
+
>
|
|
1221
|
+
{{ mdSaving ? t('viewer.saving') : (mdDirty ? t('viewer.save') : t('viewer.saved')) }}
|
|
1222
|
+
</button>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div class="fe-preview__md-split-body">
|
|
1225
|
+
<textarea
|
|
1226
|
+
class="fe-preview__md-split-input"
|
|
1227
|
+
v-model="rawText"
|
|
1228
|
+
@input="onMdInput"
|
|
1229
|
+
@keydown.ctrl.s.prevent="saveMarkdown"
|
|
1230
|
+
@keydown.meta.s.prevent="saveMarkdown"
|
|
1231
|
+
spellcheck="false"
|
|
1232
|
+
:placeholder="t('viewer.md_placeholder')"
|
|
1233
|
+
/>
|
|
1234
|
+
<div
|
|
1235
|
+
v-if="markdownUnavailable"
|
|
1236
|
+
class="fe-preview__md-split-output fe-preview__md"
|
|
1237
|
+
>{{ t('viewer.peer_not_installed') }}</div>
|
|
1238
|
+
<div
|
|
1239
|
+
v-else
|
|
1240
|
+
ref="markdownEl"
|
|
1241
|
+
class="fe-preview__md-split-output fe-preview__md"
|
|
1242
|
+
v-html="markdownHtml"
|
|
1243
|
+
></div>
|
|
1244
|
+
</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
<div v-else-if="fetchError" class="fe-preview__fallback">
|
|
1247
|
+
<p>{{ fetchError }}</p>
|
|
1248
|
+
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1249
|
+
</div>
|
|
1250
|
+
<div v-else-if="markdownHtml" ref="markdownEl" class="fe-preview__md" v-html="markdownHtml"></div>
|
|
1251
|
+
<pre v-else class="fe-preview__pre">{{ rawText }}</pre>
|
|
1252
|
+
</template>
|
|
1253
|
+
|
|
1254
|
+
<template v-else-if="kind === 'viewer'">
|
|
1255
|
+
<div v-if="viewerLoadError && !pdfFallbackToNative" class="fe-preview__fallback">
|
|
1256
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
1257
|
+
<span
|
|
1258
|
+
class="fe-preview__fallback-icon fe-preview__fallback-icon--alert"
|
|
1259
|
+
aria-hidden="true"
|
|
1260
|
+
v-html="actionIconSvg('alert')"
|
|
1261
|
+
></span>
|
|
1262
|
+
<p>{{ viewerLoadError }}</p>
|
|
1263
|
+
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div v-else-if="!viewerCmp" class="fe-preview__fallback">
|
|
1266
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
1267
|
+
<span
|
|
1268
|
+
class="fe-preview__fallback-icon fe-preview__fallback-icon--spin"
|
|
1269
|
+
aria-hidden="true"
|
|
1270
|
+
v-html="actionIconSvg('progress')"
|
|
1271
|
+
></span>
|
|
1272
|
+
<p>{{ t('viewer.loading') }}</p>
|
|
1273
|
+
</div>
|
|
1274
|
+
<component
|
|
969
1275
|
v-else
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
<
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1276
|
+
:is="viewerCmp"
|
|
1277
|
+
v-bind="viewerProps"
|
|
1278
|
+
class="fe-preview__viewer"
|
|
1279
|
+
@fallback="onPdfFallback"
|
|
1280
|
+
/>
|
|
1281
|
+
</template>
|
|
1282
|
+
|
|
1283
|
+
<template v-else-if="kind === 'code' || kind === 'text'">
|
|
1284
|
+
<div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
|
|
1285
|
+
<div v-else-if="tooLarge" class="fe-preview__fallback">
|
|
1286
|
+
{{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
|
|
1287
|
+
</div>
|
|
1288
|
+
<div v-else-if="fetchError" class="fe-preview__fallback">
|
|
1289
|
+
<p>{{ fetchError }}</p>
|
|
1290
|
+
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1291
|
+
</div>
|
|
1292
|
+
<div v-else class="fe-preview__code-wrap">
|
|
1293
|
+
<div class="fe-preview__code-toolbar">
|
|
1294
|
+
<!-- ⚠ lang="en". This badge holds a highlight.js language ID
|
|
1295
|
+
("typescript", "ini", "nginx") — a technical token, never
|
|
1296
|
+
translated prose — and the CSS uppercases it. CSS
|
|
1297
|
+
text-transform is LOCALE-AWARE: under the Turkish UI the
|
|
1298
|
+
document is lang="tr", so "typescript" uppercased to
|
|
1299
|
+
"TYPESCRİPT" with a dotted İ (measured 2026-09-13). The
|
|
1300
|
+
sibling uppercase labels (.fe-sidenav__heading and friends)
|
|
1301
|
+
hold real Turkish words where "ETİKETLER" is exactly right,
|
|
1302
|
+
so the transform stays and only this one token opts out. -->
|
|
1303
|
+
<span class="fe-preview__code-lang" lang="en">{{ codeLanguage }}</span>
|
|
1304
|
+
<span v-if="!monacoReady && codeHtml" class="fe-preview__code-status">
|
|
1305
|
+
{{ saveTextEndpoint && !monacoUnavailable ? t('viewer.editor_loading') : t('viewer.read_only') }}
|
|
1306
|
+
</span>
|
|
1307
|
+
<span v-if="saveOk" class="fe-preview__code-status fe-preview__code-status--ok">
|
|
1308
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
1309
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('check')"></span>
|
|
1310
|
+
{{ t('viewer.saved') }}
|
|
1311
|
+
</span>
|
|
1312
|
+
<span v-if="saveError" class="fe-preview__code-status fe-preview__code-status--err" :title="saveError">
|
|
1313
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
1314
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
1315
|
+
{{ t('viewer.save_error') }}
|
|
1316
|
+
</span>
|
|
1317
|
+
<span v-if="openMode === 'view'" class="fe-preview__code-status">{{ t('viewer.read_only') }}</span>
|
|
1318
|
+
<button
|
|
1319
|
+
v-else-if="saveTextEndpoint && monacoReady"
|
|
1320
|
+
type="button"
|
|
1321
|
+
class="fe-btn fe-btn--primary"
|
|
1322
|
+
:disabled="saving"
|
|
1323
|
+
@click="saveCode"
|
|
1324
|
+
>{{ saving ? t('viewer.saving') : t('viewer.save') }}</button>
|
|
1325
|
+
</div>
|
|
1326
|
+
<!-- Monaco target — hidden until ready, then occupies the slot. -->
|
|
1327
|
+
<div ref="monacoEl" class="fe-preview__code-editor" :class="{ 'is-hidden': !monacoReady }" />
|
|
1328
|
+
<!-- Highlight.js read-only fallback — visible until Monaco mounts. -->
|
|
1329
|
+
<pre
|
|
1330
|
+
v-if="!monacoReady && codeHtml"
|
|
1331
|
+
class="fe-preview__pre fe-preview__code hljs"
|
|
1332
|
+
><code :class="`language-${codeLanguage}`" v-html="codeHtml"></code></pre>
|
|
1333
|
+
<pre
|
|
1334
|
+
v-else-if="!monacoReady && !codeHtml"
|
|
1335
|
+
class="fe-preview__pre"
|
|
1336
|
+
:data-lang="codeLanguage"
|
|
1337
|
+
>{{ rawText }}</pre>
|
|
1338
|
+
</div>
|
|
1339
|
+
</template>
|
|
1340
|
+
|
|
1341
|
+
<template v-else-if="kind === 'office'">
|
|
1342
|
+
<div v-if="officeError" class="fe-preview__fallback" data-testid="office-fallback">
|
|
1343
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
1344
|
+
<span class="fe-preview__fallback-icon" aria-hidden="true" v-html="tileHtml"></span>
|
|
1345
|
+
<p>{{ officeError }}</p>
|
|
1346
|
+
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1347
|
+
</div>
|
|
1348
|
+
<div v-else ref="officeEl" class="fe-preview__office" />
|
|
1349
|
+
</template>
|
|
1350
|
+
|
|
1351
|
+
<template v-else>
|
|
1352
|
+
<div class="fe-preview__fallback">
|
|
1353
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
1354
|
+
<span class="fe-preview__fallback-icon" aria-hidden="true" v-html="tileHtml"></span>
|
|
1355
|
+
<p>{{ t('viewer.no_preview') }}</p>
|
|
1356
|
+
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1357
|
+
</div>
|
|
1358
|
+
</template>
|
|
1041
1359
|
</div>
|
|
1042
|
-
</
|
|
1360
|
+
</div>
|
|
1043
1361
|
|
|
1044
|
-
<template v-else-if="kind === 'office'">
|
|
1045
|
-
<div v-if="officeError" class="fe-preview__fallback" data-testid="office-fallback">
|
|
1046
|
-
<span class="fe-preview__fallback-icon">📄</span>
|
|
1047
|
-
<p>{{ officeError }}</p>
|
|
1048
|
-
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1049
|
-
</div>
|
|
1050
|
-
<div v-else ref="officeEl" class="fe-preview__office" />
|
|
1051
|
-
</template>
|
|
1052
|
-
|
|
1053
|
-
<template v-else>
|
|
1054
|
-
<div class="fe-preview__fallback">
|
|
1055
|
-
<span class="fe-preview__fallback-icon">📎</span>
|
|
1056
|
-
<p>{{ t('viewer.no_preview') }}</p>
|
|
1057
|
-
<a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
|
|
1058
|
-
</div>
|
|
1059
|
-
</template>
|
|
1060
|
-
</div>
|
|
1061
|
-
<template #actions>
|
|
1062
1362
|
<button
|
|
1063
|
-
v-if="
|
|
1363
|
+
v-if="canNav"
|
|
1064
1364
|
type="button"
|
|
1065
|
-
class="fe-
|
|
1066
|
-
|
|
1067
|
-
|
|
1365
|
+
class="fe-viewer__chev fe-viewer__chev--prev"
|
|
1366
|
+
:title="t('viewer.nav_prev')"
|
|
1367
|
+
:aria-label="t('viewer.nav_prev')"
|
|
1368
|
+
@click="emit('nav', -1)"
|
|
1369
|
+
>
|
|
1370
|
+
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor"
|
|
1371
|
+
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
1372
|
+
aria-hidden="true" focusable="false"><path d="M15 4.5L7.5 12 15 19.5" /></svg>
|
|
1373
|
+
</button>
|
|
1068
1374
|
<button
|
|
1069
|
-
v-if="
|
|
1375
|
+
v-if="canNav"
|
|
1070
1376
|
type="button"
|
|
1071
|
-
class="fe-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1377
|
+
class="fe-viewer__chev fe-viewer__chev--next"
|
|
1378
|
+
:title="t('viewer.nav_next')"
|
|
1379
|
+
:aria-label="t('viewer.nav_next')"
|
|
1380
|
+
@click="emit('nav', 1)"
|
|
1381
|
+
>
|
|
1382
|
+
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor"
|
|
1383
|
+
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
1384
|
+
aria-hidden="true" focusable="false"><path d="M9 4.5L16.5 12 9 19.5" /></svg>
|
|
1385
|
+
</button>
|
|
1386
|
+
|
|
1387
|
+
<!-- Zoom pill. Shown for the image viewer only - pdf, office and drawio
|
|
1388
|
+
paint their own zoom inside their surface, and a second percentage
|
|
1389
|
+
that the content does not obey is a control that lies. -->
|
|
1390
|
+
<div v-if="canZoom" class="fe-viewer__zoom">
|
|
1391
|
+
<button
|
|
1392
|
+
type="button"
|
|
1393
|
+
class="fe-viewer__zoom-btn"
|
|
1394
|
+
:disabled="!canZoomOut"
|
|
1395
|
+
:title="t('viewer.zoom_out')"
|
|
1396
|
+
:aria-label="t('viewer.zoom_out')"
|
|
1397
|
+
@click="stepZoom(-1)"
|
|
1398
|
+
>−</button>
|
|
1399
|
+
<button
|
|
1400
|
+
type="button"
|
|
1401
|
+
class="fe-viewer__zoom-level"
|
|
1402
|
+
:title="t('viewer.zoom_reset')"
|
|
1403
|
+
:aria-label="t('viewer.zoom_reset')"
|
|
1404
|
+
@click="resetZoom"
|
|
1405
|
+
>{{ zoomLabel }}</button>
|
|
1406
|
+
<button
|
|
1407
|
+
type="button"
|
|
1408
|
+
class="fe-viewer__zoom-btn"
|
|
1409
|
+
:disabled="!canZoomIn"
|
|
1410
|
+
:title="t('viewer.zoom_in')"
|
|
1411
|
+
:aria-label="t('viewer.zoom_in')"
|
|
1412
|
+
@click="stepZoom(1)"
|
|
1413
|
+
>+</button>
|
|
1414
|
+
<span class="fe-viewer__zoom-sep" aria-hidden="true"></span>
|
|
1415
|
+
<button
|
|
1416
|
+
type="button"
|
|
1417
|
+
class="fe-viewer__zoom-btn"
|
|
1418
|
+
:title="isFullscreen ? t('viewer.exit_fullscreen') : t('viewer.fullscreen')"
|
|
1419
|
+
:aria-label="isFullscreen ? t('viewer.exit_fullscreen') : t('viewer.fullscreen')"
|
|
1420
|
+
@click="toggleFullscreen"
|
|
1421
|
+
>
|
|
1422
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor"
|
|
1423
|
+
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"
|
|
1424
|
+
aria-hidden="true" focusable="false">
|
|
1425
|
+
<template v-if="isFullscreen">
|
|
1426
|
+
<path d="M9.5 4.5v5h-5" /><path d="M14.5 4.5v5h5" />
|
|
1427
|
+
<path d="M9.5 19.5v-5h-5" /><path d="M14.5 19.5v-5h5" />
|
|
1428
|
+
</template>
|
|
1429
|
+
<template v-else>
|
|
1430
|
+
<path d="M4.5 9.5v-5h5" /><path d="M19.5 9.5v-5h-5" />
|
|
1431
|
+
<path d="M4.5 14.5v5h5" /><path d="M19.5 14.5v5h-5" />
|
|
1432
|
+
</template>
|
|
1433
|
+
</svg>
|
|
1434
|
+
</button>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1077
1437
|
</Modal>
|
|
1078
1438
|
</template>
|
|
1079
1439
|
|