1<template>
2 <span>
3 <a :href="preview ? undefined : url" target="_blank">
4 <Button
5 :label="label"
6 theme="gray"
7 variant="outline"
8 @click="toggleDialog()"
9 >
10 <template #prefix>
11 <LucidePaperclip class="h-4 w-4" />
12 </template>
13 <template #suffix>
14 <slot name="suffix" />
15 </template>
16 </Button>
17 </a>
18 <Dialog v-model:open="showDialog" :title="label" size="4xl">
19 <template #default>
20 <div
21 v-if="preview === 'text'"
22 class="prose prose-sm max-w-none whitespace-pre-wrap"
23 >
24 {{ content }}
25 </div>
26 <img
27 v-if="preview === 'image'"
28 :src="url"
29 class="m-auto rounded-4 border"
30 />
31 <video
32 v-if="preview === 'video'"
33 :src="url"
34 controls
35 class="m-auto max-h-[70vh] rounded-4 border"
36 />
37 </template>
38 </Dialog>
39 </span>
40</template>
41
42<script setup lang="ts">
43import { Button, Dialog } from "frappe-ui";
44import { getType as getMime } from "mime";
45import { ref } from "vue";
46import LucidePaperclip from "~icons/lucide/paperclip";
47
48interface P {
49 label: string;
50 url?: string | null;
51}
52
53const props = withDefaults(defineProps<P>(), {
54 url: null,
55});
56
57// browsers render only some image/video mimes (.dwg is image/vnd.dwg, .avi is
58// video/x-msvideo) — anything absent here becomes a download link like pdf
59type Preview = "image" | "video" | "text";
60
61const PREVIEWS: Record<string, Preview> = {
62 "image/png": "image",
63 "image/jpeg": "image",
64 "image/gif": "image",
65 "image/webp": "image",
66 "image/svg+xml": "image",
67 "image/avif": "image",
68 "image/bmp": "image",
69 "image/apng": "image",
70 "image/vnd.microsoft.icon": "image",
71 "video/mp4": "video",
72 "video/webm": "video",
73 "video/ogg": "video",
74 "video/x-m4v": "video",
75 "text/plain": "text",
76};
77
78const showDialog = ref(false);
79const mimeType = getMime(props.label) || "";
80const preview = props.url ? PREVIEWS[mimeType] : undefined;
81const content = ref("");
82
83function toggleDialog() {
84 if (!preview) return;
85 if (preview === "text") {
86 fetch(props.url).then((res) => res.text().then((t) => (content.value = t)));
87 }
88 showDialog.value = !showDialog.value;
89}
90</script>
91