quasar-ui-danx 0.4.91 → 0.4.93

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.
Files changed (31) hide show
  1. package/dist/danx.es.js +7369 -6609
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +90 -90
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +123 -136
  8. package/src/components/Utility/Files/CarouselHeader.vue +80 -0
  9. package/src/components/Utility/Files/FilePreview.vue +76 -21
  10. package/src/components/Utility/Files/FileRenderer.vue +111 -0
  11. package/src/components/Utility/Files/ThumbnailStrip.vue +64 -0
  12. package/src/components/Utility/Files/TranscodeNavigator.vue +175 -0
  13. package/src/components/Utility/Files/VirtualCarousel.vue +237 -0
  14. package/src/components/Utility/Files/index.ts +5 -0
  15. package/src/components/Utility/Widgets/LabelPillWidget.vue +10 -0
  16. package/src/composables/index.ts +4 -0
  17. package/src/composables/useFileNavigation.ts +129 -0
  18. package/src/composables/useKeyboardNavigation.ts +58 -0
  19. package/src/composables/useThumbnailScroll.ts +43 -0
  20. package/src/composables/useVirtualCarousel.ts +93 -0
  21. package/src/helpers/filePreviewHelpers.ts +107 -0
  22. package/src/helpers/formats/datetime.ts +285 -0
  23. package/src/helpers/formats/index.ts +4 -0
  24. package/src/helpers/formats/numbers.ts +127 -0
  25. package/src/helpers/formats/parsers.ts +74 -0
  26. package/src/helpers/formats/strings.ts +65 -0
  27. package/src/helpers/formats.ts +1 -489
  28. package/src/helpers/index.ts +1 -0
  29. package/src/types/files.d.ts +38 -0
  30. package/src/types/widgets.d.ts +1 -1
  31. package/src/vue-plugin.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.91",
3
+ "version": "0.4.93",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -3,169 +3,156 @@
3
3
  :model-value="true"
4
4
  maximized
5
5
  @update:model-value="$emit('close')"
6
- @keyup.left="carousel.previous()"
7
- @keyup.right="carousel.next()"
8
6
  >
9
- <div class="absolute top-0 left-0 w-full h-full">
10
- <QCarousel
11
- ref="carousel"
12
- v-model="currentSlide"
13
- height="100%"
14
- swipeable
15
- animated
16
- :thumbnails="files.length > 1"
17
- infinite
18
- :class="cls['carousel']"
19
- >
20
- <QCarouselSlide
21
- v-for="file in files"
22
- :key="'file-' + file.id"
23
- :name="file.id"
24
- :img-src="getThumbUrl(file)"
25
- class="bg-black"
26
- >
27
- <div :class="cls['slide-image']">
28
- <template v-if="isVideo(file)">
29
- <video
30
- class="max-h-full w-full"
31
- controls
32
- >
33
- <source
34
- :src="getPreviewUrl(file) + '#t=0.1'"
35
- :type="file.mime"
36
- >
37
- </video>
38
- </template>
39
- <img
40
- v-else-if="getPreviewUrl(file)"
41
- :alt="file.filename"
42
- :src="getPreviewUrl(file)"
43
- >
7
+ <div class="absolute inset-0 bg-black">
8
+ <!-- Main Content Area -->
9
+ <div class="w-full h-full flex flex-col">
10
+ <!-- Header with filename and navigation -->
11
+ <CarouselHeader
12
+ v-if="currentFile"
13
+ :filename="currentFile.filename || currentFile.name"
14
+ :show-back-button="hasParent"
15
+ :show-transcodes-button="!!(currentFile.transcodes && currentFile.transcodes.length > 0)"
16
+ :transcodes-count="currentFile.transcodes?.length || 0"
17
+ @back="navigateToParent"
18
+ @transcodes="showTranscodeNav = true"
19
+ />
20
+
21
+ <!-- Carousel -->
22
+ <div class="flex-grow relative">
23
+ <div class="absolute inset-0">
44
24
  <div
45
- v-else-if="isText(file)"
46
- class="w-[60vw] min-w-96 bg-slate-800 rounded-lg"
25
+ v-for="slide in visibleSlides"
26
+ :key="slide.file.id"
27
+ :class="[
28
+ 'absolute inset-0 flex items-center justify-center transition-opacity duration-300',
29
+ slide.isActive ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
30
+ ]"
47
31
  >
48
- <div class="whitespace-pre-wrap p-4">
49
- {{ fileTexts[file.id] }}
50
- </div>
51
- </div>
52
- <div v-else>
53
- <h3 class="text-center mb-4">
54
- No Preview Available
55
- </h3>
56
- <a
57
- :href="file.url"
58
- target="_blank"
59
- class="text-base"
60
- >
61
- {{ file.url }}
62
- </a>
32
+ <FileRenderer
33
+ :file="slide.file"
34
+ :autoplay="slide.isActive"
35
+ />
63
36
  </div>
64
37
  </div>
65
38
 
66
- <div class="text-base text-center py-5 bg-slate-800 opacity-70 text-slate-300 absolute-top hover:opacity-20 transition-all">
67
- {{ file.filename || file.name }}
39
+ <!-- Navigation Arrows -->
40
+ <div
41
+ v-if="canNavigatePrevious"
42
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-20"
43
+ >
44
+ <QBtn
45
+ round
46
+ size="lg"
47
+ class="bg-slate-800 text-white opacity-70 hover:opacity-100"
48
+ @click="navigatePrevious"
49
+ >
50
+ <ChevronLeftIcon class="w-8" />
51
+ </QBtn>
52
+ </div>
53
+ <div
54
+ v-if="canNavigateNext"
55
+ class="absolute right-4 top-1/2 -translate-y-1/2 z-20"
56
+ >
57
+ <QBtn
58
+ round
59
+ size="lg"
60
+ class="bg-slate-800 text-white opacity-70 hover:opacity-100"
61
+ @click="navigateNext"
62
+ >
63
+ <ChevronRightIcon class="w-8" />
64
+ </QBtn>
68
65
  </div>
69
- </QCarouselSlide>
70
- </QCarousel>
66
+ </div>
67
+
68
+ <!-- Thumbnails -->
69
+ <ThumbnailStrip
70
+ :files="relatedFiles"
71
+ :current-index="currentIndex"
72
+ @navigate="navigateTo"
73
+ />
74
+ </div>
75
+
76
+ <!-- Close Button -->
71
77
  <a
72
- class="absolute top-0 right-0 text-white flex items-center justify-center w-16 h-16 hover:bg-slate-600 transition-all"
78
+ class="absolute top-0 right-0 text-white flex items-center justify-center w-16 h-16 hover:bg-slate-600 transition-all cursor-pointer z-30"
73
79
  @click="$emit('close')"
74
80
  >
75
- <CloseIcon
76
- class="w-8 h-8"
77
- />
81
+ <CloseIcon class="w-8 h-8" />
78
82
  </a>
83
+
84
+ <!-- Transcode Navigator -->
85
+ <TranscodeNavigator
86
+ v-if="currentFile?.transcodes"
87
+ v-model="showTranscodeNav"
88
+ :transcodes="currentFile.transcodes"
89
+ @select="onSelectTranscode"
90
+ />
79
91
  </div>
80
92
  </QDialog>
81
93
  </template>
94
+
82
95
  <script setup lang="ts">
83
- import { QCarousel } from "quasar";
84
- import { onMounted, ref, shallowRef } from "vue";
96
+ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/outline";
97
+ import { ref } from "vue";
98
+ import { useFileNavigation } from "../../../composables/useFileNavigation";
99
+ import { useKeyboardNavigation } from "../../../composables/useKeyboardNavigation";
100
+ import { useVirtualCarousel } from "../../../composables/useVirtualCarousel";
85
101
  import { XIcon as CloseIcon } from "../../../svg";
102
+ import { UploadedFile } from "../../../types";
103
+ import CarouselHeader from "../Files/CarouselHeader.vue";
104
+ import FileRenderer from "../Files/FileRenderer.vue";
105
+ import ThumbnailStrip from "../Files/ThumbnailStrip.vue";
106
+ import TranscodeNavigator from "../Files/TranscodeNavigator.vue";
86
107
 
87
108
  defineEmits(["close"]);
88
- const props = defineProps({
89
- files: {
90
- type: Array,
91
- default: () => []
92
- },
93
- defaultSlide: {
94
- type: String,
95
- default: ""
96
- }
97
- });
98
109
 
99
- const carousel = ref(null);
100
- const currentSlide = ref(props.defaultSlide);
101
- function isVideo(file) {
102
- return file.mime?.startsWith("video");
103
- }
110
+ const props = defineProps<{
111
+ files: UploadedFile[];
112
+ defaultSlide?: string;
113
+ }>();
104
114
 
105
- function isImage(file) {
106
- return file.mime?.startsWith("image");
107
- }
115
+ // Initialize with first file or file matching defaultSlide
116
+ const initialIndex = props.defaultSlide
117
+ ? props.files.findIndex(f => f.id === props.defaultSlide)
118
+ : 0;
119
+ const initialFile = props.files[initialIndex >= 0 ? initialIndex : 0];
108
120
 
109
- function isText(file) {
110
- return file.mime?.startsWith("text");
111
- }
121
+ // Use navigation composable
122
+ const navigation = useFileNavigation(
123
+ ref(initialFile),
124
+ ref(props.files)
125
+ );
112
126
 
113
- function getPreviewUrl(file) {
114
- // Use the optimized URL first if available. If not, use the URL directly if its an image, otherwise use the thumb URL
115
- return file.optimized?.url || (isImage(file) ? (file.blobUrl || file.url) : file.thumb?.url);
116
- }
127
+ const {
128
+ currentFile,
129
+ relatedFiles,
130
+ currentIndex,
131
+ hasParent,
132
+ canNavigatePrevious,
133
+ canNavigateNext,
134
+ navigateTo,
135
+ navigateNext,
136
+ navigatePrevious,
137
+ diveInto,
138
+ navigateToParent
139
+ } = navigation;
117
140
 
118
- function getThumbUrl(file) {
119
- if (file.thumb) {
120
- return file.thumb.url;
121
- } else if (isVideo(file)) {
122
- // Base64 encode a PlayIcon for the placeholder image
123
- return `data:image/svg+xml;base64,${btoa(
124
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>`
125
- )}`;
126
- } else {
127
- return getPreviewUrl(file) || "https://placehold.co/40x50?text=T";
128
- }
129
- }
141
+ // Use virtual carousel composable
142
+ const { visibleSlides } = useVirtualCarousel(relatedFiles, currentIndex);
130
143
 
131
- onMounted(() => {
132
- for (let file of props.files) {
133
- if (isText(file)) {
134
- loadFileText(file);
135
- }
136
- }
144
+ // Keyboard navigation
145
+ useKeyboardNavigation({
146
+ onPrevious: navigatePrevious,
147
+ onNext: navigateNext
137
148
  });
138
149
 
139
- const fileTexts = shallowRef<{ [key: string]: string }>({});
150
+ // Transcode navigation
151
+ const showTranscodeNav = ref(false);
140
152
 
141
- async function loadFileText(file) {
142
- if (fileTexts.value[file.id]) {
143
- return fileTexts.value[file.id];
153
+ function onSelectTranscode(transcode: UploadedFile, index: number) {
154
+ if (currentFile.value && currentFile.value.transcodes) {
155
+ diveInto(transcode, currentFile.value.transcodes);
144
156
  }
145
-
146
- fileTexts.value[file.id] = await fetch(file.url).then((res) => res.text());
147
157
  }
148
158
  </script>
149
- <style module="cls" lang="scss">
150
- .slide-image {
151
- width: 100%;
152
- height: 100%;
153
- background: black;
154
- display: flex;
155
- justify-content: center;
156
- align-items: center;
157
-
158
- img {
159
- max-height: 100%;
160
- max-width: 100%;
161
- object-fit: contain;
162
- }
163
- }
164
-
165
- .carousel {
166
- :deep(.q-carousel__navigation--bottom) {
167
- position: relative;
168
- bottom: 8em;
169
- }
170
- }
171
- </style>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <div
3
+ class="text-base text-center py-3 px-16 bg-slate-800 opacity-90 text-slate-300 hover:opacity-100 transition-all flex-shrink-0"
4
+ >
5
+ <div class="flex items-center justify-center gap-3">
6
+ <!-- Back to Parent Button -->
7
+ <QBtn
8
+ v-if="showBackButton"
9
+ flat
10
+ dense
11
+ class="bg-slate-700 text-slate-300 hover:bg-slate-600"
12
+ @click="$emit('back')"
13
+ >
14
+ <div class="flex items-center flex-nowrap gap-1">
15
+ <ArrowLeftIcon class="w-4" />
16
+ <span class="text-sm">Back to Parent</span>
17
+ </div>
18
+ </QBtn>
19
+
20
+ <!-- Filename -->
21
+ <div class="flex-grow">
22
+ {{ filename }}
23
+ </div>
24
+
25
+ <!-- Transcodes Button -->
26
+ <QBtn
27
+ v-if="showTranscodesButton"
28
+ flat
29
+ dense
30
+ class="bg-purple-700 text-purple-200 hover:bg-purple-600"
31
+ @click="$emit('transcodes')"
32
+ >
33
+ <div class="flex items-center flex-nowrap gap-1">
34
+ <FilmIcon class="w-4" />
35
+ <QBadge
36
+ class="bg-purple-900 text-purple-200"
37
+ :label="transcodesCount"
38
+ />
39
+ <span class="text-sm ml-1">Transcodes</span>
40
+ </div>
41
+ </QBtn>
42
+
43
+ <!-- Close Button (optional) -->
44
+ <QBtn
45
+ v-if="showCloseButton"
46
+ flat
47
+ dense
48
+ icon
49
+ class="text-slate-300 hover:text-white"
50
+ @click="$emit('close')"
51
+ >
52
+ <CloseIcon class="w-5" />
53
+ </QBtn>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { ArrowLeftIcon, FilmIcon } from "@heroicons/vue/outline";
60
+ import { XIcon as CloseIcon } from "../../../svg";
61
+
62
+ withDefaults(defineProps<{
63
+ filename: string;
64
+ showBackButton?: boolean;
65
+ showTranscodesButton?: boolean;
66
+ transcodesCount?: number;
67
+ showCloseButton?: boolean;
68
+ }>(), {
69
+ showBackButton: false,
70
+ showTranscodesButton: false,
71
+ transcodesCount: 0,
72
+ showCloseButton: false
73
+ });
74
+
75
+ defineEmits<{
76
+ 'back': [];
77
+ 'transcodes': [];
78
+ 'close': [];
79
+ }>();
80
+ </script>
@@ -103,6 +103,22 @@
103
103
  </template>
104
104
 
105
105
  <div class="absolute top-1 right-1 flex items-center flex-nowrap justify-between space-x-1 transition-all opacity-0 group-hover:opacity-100">
106
+ <QBtn
107
+ v-if="hasTranscodes"
108
+ :size="btnSize"
109
+ class="dx-file-preview-transcodes bg-purple-700 text-white opacity-70 hover:opacity-100 py-1 px-2 relative"
110
+ @click.stop="showPreview = true"
111
+ >
112
+ <div class="flex items-center flex-nowrap gap-1">
113
+ <FilmIcon class="w-4 h-5" />
114
+ <QBadge
115
+ class="bg-purple-900 text-purple-200"
116
+ :label="file?.transcodes?.length || 0"
117
+ />
118
+ </div>
119
+ <QTooltip>View Transcodes</QTooltip>
120
+ </QBtn>
121
+
106
122
  <QBtn
107
123
  v-if="downloadable && computedImage?.url"
108
124
  :size="btnSize"
@@ -142,10 +158,12 @@
142
158
  </template>
143
159
 
144
160
  <script setup lang="ts">
145
- import { DocumentTextIcon as TextFileIcon, DownloadIcon, PlayIcon } from "@heroicons/vue/outline";
146
- import { computed, ComputedRef, ref, shallowRef } from "vue";
161
+ import { DocumentTextIcon as TextFileIcon, DownloadIcon, FilmIcon, PlayIcon } from "@heroicons/vue/outline";
162
+ import { computed, ComputedRef, onMounted, ref, watch } from "vue";
147
163
  import { danxOptions } from "../../../config";
148
164
  import { download, uniqueBy } from "../../../helpers";
165
+ import * as fileHelpers from "../../../helpers/filePreviewHelpers";
166
+ import { getMimeType, getOptimizedUrl } from "../../../helpers/filePreviewHelpers";
149
167
  import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
150
168
  import { UploadedFile } from "../../../types";
151
169
  import { FullScreenCarouselDialog } from "../Dialogs";
@@ -172,7 +190,6 @@ export interface FilePreviewProps {
172
190
  disabled?: boolean;
173
191
  square?: boolean;
174
192
  btnSize?: "xs" | "sm" | "md" | "lg";
175
- showTranscodes?: boolean;
176
193
  }
177
194
 
178
195
  const emit = defineEmits(["remove"]);
@@ -193,6 +210,7 @@ const props = withDefaults(defineProps<FilePreviewProps>(), {
193
210
 
194
211
 
195
212
  const showPreview = ref(false);
213
+ const isLoadingTranscodes = ref(false);
196
214
  const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
197
215
  if (props.file) {
198
216
  return props.file;
@@ -209,11 +227,11 @@ const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
209
227
  return null;
210
228
  });
211
229
 
212
- const transcodes = shallowRef(props.file?.transcodes || null);
213
230
  const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
214
231
  const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
232
+ const hasTranscodes = computed(() => (props.file?.transcodes?.length || 0) > 0);
215
233
  const previewableFiles: ComputedRef<(UploadedFile | null)[] | null> = computed(() => {
216
- return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...(props.showTranscodes ? (transcodes.value || []) : props.relatedFiles)], filesHaveSameUrl) : [computedImage.value];
234
+ return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl) : [computedImage.value];
217
235
  });
218
236
 
219
237
  function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
@@ -223,18 +241,12 @@ function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
223
241
  }
224
242
 
225
243
  const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
226
- const mimeType = computed(
227
- () => computedImage.value?.type || computedImage.value?.mime || ""
228
- );
229
- const isImage = computed(() => !!mimeType.value.match(/^image\//));
230
- const isVideo = computed(() => !!mimeType.value.match(/^video\//));
231
- const isPdf = computed(() => !!mimeType.value.match(/^application\/pdf/));
232
- const previewUrl = computed(
233
- () => computedImage.value?.optimized?.url || computedImage.value?.blobUrl || computedImage.value?.url
234
- );
235
- const thumbUrl = computed(() => {
236
- return computedImage.value?.thumb?.url;
237
- });
244
+ const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
245
+ const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
246
+ const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
247
+ const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
248
+ const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
249
+ const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
238
250
  const isPreviewable = computed(() => {
239
251
  return !!thumbUrl.value || isVideo.value || isImage.value;
240
252
  });
@@ -268,14 +280,57 @@ function onRemove() {
268
280
  }
269
281
  }
270
282
 
271
- async function onShowPreview() {
283
+ function onShowPreview() {
272
284
  showPreview.value = true;
285
+ }
286
+
287
+ /**
288
+ * Check if transcodes need to be loaded for the current file
289
+ */
290
+ function shouldLoadTranscodes(): boolean {
291
+ if (!props.file?.id) return false;
292
+ if (isLoadingTranscodes.value) return false;
293
+ if (!danxOptions.value.fileUpload?.refreshFile) return false;
273
294
 
274
- if (props.showTranscodes && props.file && !transcodes.value) {
275
- const file = await danxOptions.value.fileUpload.refreshFile(props.file.id) as UploadedFile;
276
- transcodes.value = file.transcodes || [];
295
+ // Only load if transcodes is explicitly null, undefined, or an empty array
296
+ const transcodes = props.file.transcodes;
297
+ return transcodes === null || transcodes === undefined || (Array.isArray(transcodes) && transcodes.length === 0);
298
+ }
299
+
300
+ /**
301
+ * Load transcodes for the current file
302
+ */
303
+ async function loadTranscodes() {
304
+ if (!shouldLoadTranscodes()) return;
305
+
306
+ isLoadingTranscodes.value = true;
307
+
308
+ try {
309
+ const refreshFile = danxOptions.value.fileUpload.refreshFile;
310
+ if (refreshFile && props.file?.id) {
311
+ const refreshedFile = await refreshFile(props.file.id);
312
+
313
+ // Update the file object with the loaded transcodes
314
+ if (refreshedFile.transcodes && props.file) {
315
+ props.file.transcodes = refreshedFile.transcodes;
316
+ }
317
+ }
318
+ } catch (error) {
319
+ console.error("Failed to load transcodes:", error);
320
+ } finally {
321
+ isLoadingTranscodes.value = false;
277
322
  }
278
323
  }
324
+
325
+ // Load transcodes when component mounts
326
+ onMounted(() => {
327
+ loadTranscodes();
328
+ });
329
+
330
+ // Watch for file changes and reload transcodes if needed
331
+ watch(() => props.file?.id, () => {
332
+ loadTranscodes();
333
+ });
279
334
  </script>
280
335
 
281
336
  <style module="cls" lang="scss">
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div class="file-renderer flex items-center justify-center w-full h-full">
3
+ <!-- Video -->
4
+ <template v-if="isVideo(file)">
5
+ <video
6
+ class="max-h-full w-full"
7
+ controls
8
+ :autoplay="autoplay"
9
+ >
10
+ <source
11
+ :src="getPreviewUrl(file) + '#t=0.1'"
12
+ :type="file.mime"
13
+ >
14
+ </video>
15
+ </template>
16
+
17
+ <!-- Image -->
18
+ <img
19
+ v-else-if="getPreviewUrl(file)"
20
+ :alt="file.filename || file.name"
21
+ :src="getPreviewUrl(file)"
22
+ class="max-h-full max-w-full object-contain"
23
+ >
24
+
25
+ <!-- Text File (lazy loaded) -->
26
+ <div
27
+ v-else-if="isText(file)"
28
+ class="w-[60vw] min-w-96 max-h-[80vh] bg-slate-800 rounded-lg overflow-auto"
29
+ >
30
+ <div class="whitespace-pre-wrap p-4 text-slate-200">
31
+ <template v-if="textContent">
32
+ {{ textContent }}
33
+ </template>
34
+ <div
35
+ v-else
36
+ class="flex items-center justify-center py-8"
37
+ >
38
+ <QSpinnerPie
39
+ class="text-slate-400"
40
+ size="48px"
41
+ />
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- No Preview -->
47
+ <div
48
+ v-else
49
+ class="text-center"
50
+ >
51
+ <h3 class="text-slate-300 mb-4">
52
+ No Preview Available
53
+ </h3>
54
+ <a
55
+ :href="file.url"
56
+ target="_blank"
57
+ class="text-blue-400 hover:text-blue-300"
58
+ >
59
+ {{ file.url }}
60
+ </a>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { QSpinnerPie } from "quasar";
67
+ import { onMounted, ref, watch } from "vue";
68
+ import { getPreviewUrl, isText, isVideo } from "../../../helpers/filePreviewHelpers";
69
+ import { UploadedFile } from "../../../types";
70
+
71
+ const props = withDefaults(defineProps<{
72
+ file: UploadedFile;
73
+ autoplay?: boolean;
74
+ loadText?: boolean;
75
+ }>(), {
76
+ autoplay: false,
77
+ loadText: true
78
+ });
79
+
80
+ const textContent = ref<string>("");
81
+
82
+ /**
83
+ * Load text file content from URL
84
+ */
85
+ async function loadFileText() {
86
+ if (!isText(props.file) || !props.loadText) {
87
+ return;
88
+ }
89
+
90
+ if (textContent.value) {
91
+ return; // Already loaded
92
+ }
93
+
94
+ try {
95
+ const text = await fetch(props.file.url || "").then((res) => res.text());
96
+ textContent.value = text;
97
+ } catch (e) {
98
+ textContent.value = "Error loading file content";
99
+ }
100
+ }
101
+
102
+ // Load text content on mount and when file changes
103
+ onMounted(() => {
104
+ loadFileText();
105
+ });
106
+
107
+ watch(() => props.file.id, () => {
108
+ textContent.value = ""; // Reset
109
+ loadFileText();
110
+ });
111
+ </script>