223 lines
6.8 KiB
Vue
223 lines
6.8 KiB
Vue
<script setup>
|
|
import { ref, computed, watch } from "vue";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/Components/ui/dialog";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Badge } from "../ui/badge";
|
|
import { Loader2 } from "lucide-vue-next";
|
|
import axios from "axios";
|
|
|
|
const props = defineProps({
|
|
show: { type: Boolean, default: false },
|
|
src: { type: String, default: "" },
|
|
title: { type: String, default: "Dokument" },
|
|
mimeType: { type: String, default: "" },
|
|
filename: { type: String, default: "" },
|
|
});
|
|
const emit = defineEmits(["close"]);
|
|
|
|
const textContent = ref("");
|
|
const loading = ref(false);
|
|
const previewGenerating = ref(false);
|
|
const previewError = ref("");
|
|
|
|
const fileExtension = computed(() => {
|
|
if (props.filename) {
|
|
return props.filename.split(".").pop()?.toLowerCase() || "";
|
|
}
|
|
return "";
|
|
});
|
|
|
|
const viewerType = computed(() => {
|
|
const ext = fileExtension.value;
|
|
const mime = props.mimeType.toLowerCase();
|
|
|
|
if (ext === "pdf" || mime === "application/pdf") return "pdf";
|
|
// DOCX/DOC files are converted to PDF by backend - treat as PDF viewer
|
|
if (["doc", "docx"].includes(ext) || mime.includes("word") || mime.includes("msword"))
|
|
return "docx";
|
|
if (["jpg", "jpeg", "png", "gif", "webp"].includes(ext) || mime.startsWith("image/"))
|
|
return "image";
|
|
if (["txt", "csv", "xml"].includes(ext) || mime.startsWith("text/")) return "text";
|
|
|
|
return "unsupported";
|
|
});
|
|
|
|
const loadTextContent = async () => {
|
|
if (!props.src || viewerType.value !== "text") return;
|
|
loading.value = true;
|
|
try {
|
|
const response = await axios.get(props.src);
|
|
textContent.value = response.data;
|
|
} catch (e) {
|
|
textContent.value = "Napaka pri nalaganju vsebine.";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// For DOCX files, the backend converts to PDF. If the preview isn't ready yet (202 status),
|
|
// we poll until it's available.
|
|
const docxPreviewUrl = ref("");
|
|
const loadDocxPreview = async () => {
|
|
if (!props.src || viewerType.value !== "docx") return;
|
|
|
|
previewGenerating.value = true;
|
|
previewError.value = "";
|
|
docxPreviewUrl.value = "";
|
|
|
|
const maxRetries = 15;
|
|
const retryDelay = 2000; // 2 seconds between retries
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
const response = await axios.head(props.src, { validateStatus: () => true });
|
|
|
|
if (response.status >= 200 && response.status < 300) {
|
|
// Preview is ready
|
|
docxPreviewUrl.value = props.src;
|
|
previewGenerating.value = false;
|
|
return;
|
|
} else if (response.status === 202) {
|
|
// Preview is being generated, wait and retry
|
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
} else {
|
|
// Other error
|
|
previewError.value = "Napaka pri nalaganju predogleda.";
|
|
previewGenerating.value = false;
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
previewError.value = "Napaka pri nalaganju predogleda.";
|
|
previewGenerating.value = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Max retries reached
|
|
previewError.value = "Predogled ni na voljo. Prosimo poskusite znova kasneje.";
|
|
previewGenerating.value = false;
|
|
};
|
|
|
|
watch(
|
|
() => [props.show, props.src],
|
|
([show]) => {
|
|
if (show && viewerType.value === "text") {
|
|
loadTextContent();
|
|
}
|
|
if (show && viewerType.value === "docx") {
|
|
loadDocxPreview();
|
|
}
|
|
// Reset states when dialog closes
|
|
if (!show) {
|
|
previewGenerating.value = false;
|
|
previewError.value = "";
|
|
docxPreviewUrl.value = "";
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog :open="show" @update:open="(open) => !open && $emit('close')">
|
|
<DialogContent class="max-w-full xl:max-w-7xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{{ title }}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
<Badge>
|
|
{{ fileExtension }}
|
|
</Badge>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div class="h-[70vh] overflow-auto">
|
|
<!-- PDF Viewer (browser native) -->
|
|
<template v-if="viewerType === 'pdf' && props.src">
|
|
<iframe
|
|
:src="props.src"
|
|
class="w-full h-full rounded border"
|
|
type="application/pdf"
|
|
/>
|
|
</template>
|
|
|
|
<!-- DOCX Viewer (converted to PDF by backend) -->
|
|
<template v-else-if="viewerType === 'docx'">
|
|
<!-- Loading/generating state -->
|
|
<div
|
|
v-if="previewGenerating"
|
|
class="flex flex-col items-center justify-center h-full gap-4"
|
|
>
|
|
<Loader2 class="h-8 w-8 animate-spin text-indigo-600" />
|
|
<span class="text-gray-500">Priprava predogleda dokumenta...</span>
|
|
</div>
|
|
<!-- Error state -->
|
|
<div
|
|
v-else-if="previewError"
|
|
class="flex flex-col items-center justify-center h-full gap-4 text-gray-500"
|
|
>
|
|
<span>{{ previewError }}</span>
|
|
<Button as="a" :href="props.src" target="_blank" variant="outline">
|
|
Prenesi datoteko
|
|
</Button>
|
|
</div>
|
|
<!-- Preview ready -->
|
|
<iframe
|
|
v-else-if="docxPreviewUrl"
|
|
:src="docxPreviewUrl"
|
|
class="w-full h-full rounded border"
|
|
type="application/pdf"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Image Viewer -->
|
|
<template v-else-if="viewerType === 'image' && props.src">
|
|
<img
|
|
:src="props.src"
|
|
:alt="props.title"
|
|
class="max-w-full max-h-full mx-auto object-contain"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Text/CSV/XML Viewer -->
|
|
<template v-else-if="viewerType === 'text'">
|
|
<div v-if="loading" class="flex items-center justify-center h-full">
|
|
<div class="animate-pulse text-gray-500">Nalaganje...</div>
|
|
</div>
|
|
<pre
|
|
v-else
|
|
class="p-4 bg-gray-50 dark:bg-gray-900 rounded border text-sm overflow-auto h-full whitespace-pre-wrap wrap-break-word"
|
|
>{{ textContent }}</pre
|
|
>
|
|
</template>
|
|
|
|
<!-- Unsupported -->
|
|
<template v-else-if="viewerType === 'unsupported'">
|
|
<div
|
|
class="flex flex-col items-center justify-center h-full gap-4 text-gray-500"
|
|
>
|
|
<span>Predogled ni na voljo za to vrsto datoteke.</span>
|
|
<Button as="a" :href="props.src" target="_blank" variant="outline">
|
|
Prenesi datoteko
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- No source -->
|
|
<div v-else class="text-sm text-gray-500">Ni dokumenta za prikaz.</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-4">
|
|
<Button type="button" variant="outline" @click="$emit('close')">Zapri</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|