quasar-ui-danx 0.4.92 → 0.4.94
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/dist/danx.es.js +7149 -6414
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +90 -90
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/Form/Fields/NumberField.vue +36 -8
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +123 -136
- package/src/components/Utility/Files/CarouselHeader.vue +80 -0
- package/src/components/Utility/Files/FilePreview.vue +76 -21
- package/src/components/Utility/Files/FileRenderer.vue +111 -0
- package/src/components/Utility/Files/ThumbnailStrip.vue +64 -0
- package/src/components/Utility/Files/TranscodeNavigator.vue +175 -0
- package/src/components/Utility/Files/VirtualCarousel.vue +237 -0
- package/src/components/Utility/Files/index.ts +5 -0
- package/src/components/Utility/Widgets/LabelPillWidget.vue +10 -0
- package/src/composables/index.ts +4 -0
- package/src/composables/useFileNavigation.ts +129 -0
- package/src/composables/useKeyboardNavigation.ts +58 -0
- package/src/composables/useThumbnailScroll.ts +43 -0
- package/src/composables/useVirtualCarousel.ts +93 -0
- package/src/helpers/filePreviewHelpers.ts +107 -0
- package/src/helpers/formats/parsers.ts +5 -2
- package/src/helpers/index.ts +1 -0
- package/src/types/files.d.ts +38 -0
- package/src/types/widgets.d.ts +1 -1
- package/src/vue-plugin.ts +1 -0
package/package.json
CHANGED
|
@@ -50,7 +50,37 @@ function format(number) {
|
|
|
50
50
|
return fNumber(number, options);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const onUpdateDebounced = useDebounceFn((val: number | string | undefined) =>
|
|
53
|
+
const onUpdateDebounced = useDebounceFn((val: number | string | undefined) => {
|
|
54
|
+
// Apply min/max clamping to the final value
|
|
55
|
+
let clampedVal = val;
|
|
56
|
+
if (typeof val === "number") {
|
|
57
|
+
if (props.min !== undefined) {
|
|
58
|
+
clampedVal = Math.max(val, props.min);
|
|
59
|
+
}
|
|
60
|
+
if (props.max !== undefined) {
|
|
61
|
+
clampedVal = Math.min(clampedVal as number, props.max);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
emit("update", clampedVal);
|
|
65
|
+
}, props.delay);
|
|
66
|
+
|
|
67
|
+
const applyMinMaxClamping = useDebounceFn((value: number | undefined) => {
|
|
68
|
+
if (value === undefined) return;
|
|
69
|
+
|
|
70
|
+
let clampedValue = value;
|
|
71
|
+
if (props.min !== undefined) {
|
|
72
|
+
clampedValue = Math.max(clampedValue, props.min);
|
|
73
|
+
}
|
|
74
|
+
if (props.max !== undefined) {
|
|
75
|
+
clampedValue = Math.min(clampedValue, props.max);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Only update the display if the value changed after clamping
|
|
79
|
+
if (clampedValue !== value) {
|
|
80
|
+
numberVal.value = format(clampedValue);
|
|
81
|
+
emit("update:model-value", clampedValue);
|
|
82
|
+
}
|
|
83
|
+
}, 500);
|
|
54
84
|
|
|
55
85
|
function onInput(value) {
|
|
56
86
|
let number: number | undefined = undefined;
|
|
@@ -68,19 +98,17 @@ function onInput(value) {
|
|
|
68
98
|
value = value.replace(/[^\d.]/g, "");
|
|
69
99
|
number = +value;
|
|
70
100
|
|
|
71
|
-
|
|
72
|
-
number = Math.max(number, props.min);
|
|
73
|
-
}
|
|
74
|
-
if (props.max) {
|
|
75
|
-
number = Math.min(number, props.max);
|
|
76
|
-
}
|
|
77
|
-
|
|
101
|
+
// Don't clamp immediately - let user type freely
|
|
78
102
|
numberVal.value = format(number);
|
|
79
103
|
}
|
|
80
104
|
|
|
105
|
+
// Emit the raw typed value immediately
|
|
81
106
|
emit("update:model-value", number);
|
|
82
107
|
|
|
83
108
|
// Delay the change event, so we only see the value after the user has finished
|
|
84
109
|
onUpdateDebounced(number);
|
|
110
|
+
|
|
111
|
+
// Apply min/max clamping after user stops typing
|
|
112
|
+
applyMinMaxClamping(number);
|
|
85
113
|
}
|
|
86
114
|
</script>
|
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class="
|
|
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-
|
|
46
|
-
|
|
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
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
</
|
|
70
|
-
|
|
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 {
|
|
84
|
-
import {
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
110
|
+
const props = defineProps<{
|
|
111
|
+
files: UploadedFile[];
|
|
112
|
+
defaultSlide?: string;
|
|
113
|
+
}>();
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
// Use navigation composable
|
|
122
|
+
const navigation = useFileNavigation(
|
|
123
|
+
ref(initialFile),
|
|
124
|
+
ref(props.files)
|
|
125
|
+
);
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
}
|
|
144
|
+
// Keyboard navigation
|
|
145
|
+
useKeyboardNavigation({
|
|
146
|
+
onPrevious: navigatePrevious,
|
|
147
|
+
onNext: navigateNext
|
|
137
148
|
});
|
|
138
149
|
|
|
139
|
-
|
|
150
|
+
// Transcode navigation
|
|
151
|
+
const showTranscodeNav = ref(false);
|
|
140
152
|
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
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,
|
|
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, ...
|
|
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
|
-
|
|
228
|
-
);
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
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
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
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">
|