quasar-ui-danx 0.4.95 → 0.4.99
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 +24452 -22880
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +133 -122
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Buttons/ActionButton.vue +11 -3
- package/src/components/Utility/Code/CodeViewer.vue +219 -0
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
- package/src/components/Utility/Code/LanguageBadge.vue +122 -0
- package/src/components/Utility/Code/MarkdownContent.vue +251 -0
- package/src/components/Utility/Code/index.ts +5 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
- package/src/components/Utility/Files/CarouselHeader.vue +24 -0
- package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
- package/src/components/Utility/Files/FilePreview.vue +118 -166
- package/src/components/Utility/Files/index.ts +1 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useCodeFormat.ts +199 -0
- package/src/composables/useCodeViewerCollapse.ts +125 -0
- package/src/composables/useCodeViewerEditor.ts +420 -0
- package/src/composables/useFilePreview.ts +119 -0
- package/src/composables/useTranscodeLoader.ts +68 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/renderMarkdown.ts +338 -0
- package/src/styles/danx.scss +3 -0
- package/src/styles/themes/danx/code.scss +158 -0
- package/src/styles/themes/danx/index.scss +2 -0
- package/src/styles/themes/danx/markdown.scss +145 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { computed, nextTick, onUnmounted, Ref, ref } from "vue";
|
|
2
|
+
import { CodeFormat, UseCodeFormatReturn, ValidationError } from "./useCodeFormat";
|
|
3
|
+
import { highlightSyntax } from "../helpers/formats/highlightSyntax";
|
|
4
|
+
|
|
5
|
+
export interface UseCodeViewerEditorOptions {
|
|
6
|
+
codeRef: Ref<HTMLPreElement | null>;
|
|
7
|
+
codeFormat: UseCodeFormatReturn;
|
|
8
|
+
currentFormat: Ref<CodeFormat>;
|
|
9
|
+
canEdit: Ref<boolean>;
|
|
10
|
+
editable: Ref<boolean>;
|
|
11
|
+
onEmitModelValue: (value: object | string | null) => void;
|
|
12
|
+
onEmitEditable: (editable: boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseCodeViewerEditorReturn {
|
|
16
|
+
// State
|
|
17
|
+
internalEditable: Ref<boolean>;
|
|
18
|
+
editingContent: Ref<string>;
|
|
19
|
+
cachedHighlightedContent: Ref<string>;
|
|
20
|
+
isUserEditing: Ref<boolean>;
|
|
21
|
+
validationError: Ref<ValidationError | null>;
|
|
22
|
+
|
|
23
|
+
// Computed
|
|
24
|
+
isEditing: Ref<boolean>;
|
|
25
|
+
hasValidationError: Ref<boolean>;
|
|
26
|
+
highlightedContent: Ref<string>;
|
|
27
|
+
displayContent: Ref<string>;
|
|
28
|
+
charCount: Ref<number>;
|
|
29
|
+
isValid: Ref<boolean>;
|
|
30
|
+
|
|
31
|
+
// Methods
|
|
32
|
+
toggleEdit: () => void;
|
|
33
|
+
onContentEditableInput: (event: Event) => void;
|
|
34
|
+
onContentEditableBlur: () => void;
|
|
35
|
+
onKeyDown: (event: KeyboardEvent) => void;
|
|
36
|
+
syncEditableFromProp: (value: boolean) => void;
|
|
37
|
+
syncEditingContentFromValue: () => void;
|
|
38
|
+
updateEditingContentOnFormatChange: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get cursor offset in plain text within a contenteditable element
|
|
43
|
+
*/
|
|
44
|
+
function getCursorOffset(codeRef: HTMLPreElement | null): number {
|
|
45
|
+
const selection = window.getSelection();
|
|
46
|
+
if (!selection || !selection.rangeCount || !codeRef) return 0;
|
|
47
|
+
|
|
48
|
+
const range = selection.getRangeAt(0);
|
|
49
|
+
const preCaretRange = document.createRange();
|
|
50
|
+
preCaretRange.selectNodeContents(codeRef);
|
|
51
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
52
|
+
|
|
53
|
+
// Count characters by walking text nodes
|
|
54
|
+
let offset = 0;
|
|
55
|
+
const walker = document.createTreeWalker(preCaretRange.commonAncestorContainer, NodeFilter.SHOW_TEXT);
|
|
56
|
+
let node = walker.nextNode();
|
|
57
|
+
while (node) {
|
|
58
|
+
if (preCaretRange.intersectsNode(node)) {
|
|
59
|
+
const nodeRange = document.createRange();
|
|
60
|
+
nodeRange.selectNodeContents(node);
|
|
61
|
+
if (preCaretRange.compareBoundaryPoints(Range.END_TO_END, nodeRange) >= 0) {
|
|
62
|
+
offset += node.textContent?.length || 0;
|
|
63
|
+
} else {
|
|
64
|
+
// Partial node - cursor is in this node
|
|
65
|
+
offset += range.startOffset;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
node = walker.nextNode();
|
|
70
|
+
}
|
|
71
|
+
return offset;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set cursor to offset in plain text within a contenteditable element
|
|
76
|
+
*/
|
|
77
|
+
function setCursorOffset(codeRef: HTMLPreElement | null, targetOffset: number): void {
|
|
78
|
+
if (!codeRef) return;
|
|
79
|
+
|
|
80
|
+
const selection = window.getSelection();
|
|
81
|
+
if (!selection) return;
|
|
82
|
+
|
|
83
|
+
let currentOffset = 0;
|
|
84
|
+
const walker = document.createTreeWalker(codeRef, NodeFilter.SHOW_TEXT);
|
|
85
|
+
let node = walker.nextNode();
|
|
86
|
+
|
|
87
|
+
while (node) {
|
|
88
|
+
const nodeLength = node.textContent?.length || 0;
|
|
89
|
+
if (currentOffset + nodeLength >= targetOffset) {
|
|
90
|
+
const range = document.createRange();
|
|
91
|
+
range.setStart(node, targetOffset - currentOffset);
|
|
92
|
+
range.collapse(true);
|
|
93
|
+
selection.removeAllRanges();
|
|
94
|
+
selection.addRange(range);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
currentOffset += nodeLength;
|
|
98
|
+
node = walker.nextNode();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If offset not found, place at end
|
|
102
|
+
const range = document.createRange();
|
|
103
|
+
range.selectNodeContents(codeRef);
|
|
104
|
+
range.collapse(false);
|
|
105
|
+
selection.removeAllRanges();
|
|
106
|
+
selection.addRange(range);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get current line info from cursor position
|
|
111
|
+
*/
|
|
112
|
+
function getCurrentLineInfo(editingContent: string, codeRef: HTMLPreElement | null): { indent: string; lineContent: string } | null {
|
|
113
|
+
const text = editingContent;
|
|
114
|
+
if (!text) return { indent: "", lineContent: "" };
|
|
115
|
+
|
|
116
|
+
// Get cursor position in plain text
|
|
117
|
+
const cursorOffset = getCursorOffset(codeRef);
|
|
118
|
+
|
|
119
|
+
// Find the start of the current line (after the previous newline)
|
|
120
|
+
const textBeforeCursor = text.substring(0, cursorOffset);
|
|
121
|
+
const lastNewlineIndex = textBeforeCursor.lastIndexOf("\n");
|
|
122
|
+
const lineStart = lastNewlineIndex + 1;
|
|
123
|
+
|
|
124
|
+
// Get the content from line start to cursor
|
|
125
|
+
const lineContent = textBeforeCursor.substring(lineStart);
|
|
126
|
+
|
|
127
|
+
// Extract indentation (spaces/tabs at start of line)
|
|
128
|
+
const indentMatch = lineContent.match(/^[\t ]*/);
|
|
129
|
+
const indent = indentMatch ? indentMatch[0] : "";
|
|
130
|
+
|
|
131
|
+
return { indent, lineContent };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Calculate smart indentation based on context
|
|
136
|
+
*/
|
|
137
|
+
function getSmartIndent(lineInfo: { indent: string; lineContent: string }, format: CodeFormat): string {
|
|
138
|
+
const { indent, lineContent } = lineInfo;
|
|
139
|
+
const trimmedLine = lineContent.trim();
|
|
140
|
+
const indentUnit = " "; // 2 spaces
|
|
141
|
+
|
|
142
|
+
if (format === "yaml") {
|
|
143
|
+
// After a key with colon (e.g., "key:" or "key: |" or "key: >")
|
|
144
|
+
if (trimmedLine.endsWith(":") || trimmedLine.match(/:\s*[|>][-+]?\s*$/)) {
|
|
145
|
+
return indent + indentUnit;
|
|
146
|
+
}
|
|
147
|
+
// After array item start (e.g., "- item" or "- key: value")
|
|
148
|
+
if (trimmedLine.startsWith("- ")) {
|
|
149
|
+
return indent + indentUnit;
|
|
150
|
+
}
|
|
151
|
+
// Just "- " alone means we want to continue with same array indentation
|
|
152
|
+
if (trimmedLine === "-") {
|
|
153
|
+
return indent;
|
|
154
|
+
}
|
|
155
|
+
} else if (format === "json") {
|
|
156
|
+
// After opening brace/bracket
|
|
157
|
+
if (trimmedLine.endsWith("{") || trimmedLine.endsWith("[")) {
|
|
158
|
+
return indent + indentUnit;
|
|
159
|
+
}
|
|
160
|
+
// After comma, maintain indentation
|
|
161
|
+
if (trimmedLine.endsWith(",")) {
|
|
162
|
+
return indent;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default: maintain current indentation
|
|
167
|
+
return indent;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Composable for CodeViewer editor functionality
|
|
172
|
+
*/
|
|
173
|
+
export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCodeViewerEditorReturn {
|
|
174
|
+
const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable } = options;
|
|
175
|
+
|
|
176
|
+
// Debounce timeout handles
|
|
177
|
+
let validationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
178
|
+
let highlightTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
179
|
+
|
|
180
|
+
// Local state
|
|
181
|
+
const internalEditable = ref(editable.value);
|
|
182
|
+
const editingContent = ref("");
|
|
183
|
+
const cachedHighlightedContent = ref("");
|
|
184
|
+
const isUserEditing = ref(false);
|
|
185
|
+
const validationError = ref<ValidationError | null>(null);
|
|
186
|
+
|
|
187
|
+
// Computed: has validation error
|
|
188
|
+
const hasValidationError = computed(() => validationError.value !== null);
|
|
189
|
+
|
|
190
|
+
// Computed: is currently in edit mode
|
|
191
|
+
const isEditing = computed(() => canEdit.value && internalEditable.value);
|
|
192
|
+
|
|
193
|
+
// Computed: display content
|
|
194
|
+
const displayContent = computed(() => {
|
|
195
|
+
if (isUserEditing.value) {
|
|
196
|
+
return editingContent.value;
|
|
197
|
+
}
|
|
198
|
+
return codeFormat.formattedContent.value;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Computed: highlighted content with syntax highlighting
|
|
202
|
+
const highlightedContent = computed(() => {
|
|
203
|
+
if (isUserEditing.value) {
|
|
204
|
+
return cachedHighlightedContent.value;
|
|
205
|
+
}
|
|
206
|
+
const highlighted = highlightSyntax(displayContent.value, { format: currentFormat.value });
|
|
207
|
+
cachedHighlightedContent.value = highlighted;
|
|
208
|
+
return highlighted;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Computed: is current content valid
|
|
212
|
+
const isValid = computed(() => {
|
|
213
|
+
if (hasValidationError.value) return false;
|
|
214
|
+
return codeFormat.isValid.value;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Computed: character count
|
|
218
|
+
const charCount = computed(() => {
|
|
219
|
+
return displayContent.value?.length || 0;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Sync internal editable state with prop
|
|
223
|
+
function syncEditableFromProp(value: boolean): void {
|
|
224
|
+
internalEditable.value = value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Sync editing content when external value changes
|
|
228
|
+
function syncEditingContentFromValue(): void {
|
|
229
|
+
if (!isUserEditing.value) {
|
|
230
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Update editing content when format changes
|
|
235
|
+
function updateEditingContentOnFormatChange(): void {
|
|
236
|
+
if (isEditing.value) {
|
|
237
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Debounced validation
|
|
242
|
+
function debouncedValidate(): void {
|
|
243
|
+
if (validationTimeout) {
|
|
244
|
+
clearTimeout(validationTimeout);
|
|
245
|
+
}
|
|
246
|
+
validationTimeout = setTimeout(() => {
|
|
247
|
+
validationError.value = codeFormat.validateWithError(editingContent.value, currentFormat.value);
|
|
248
|
+
}, 300);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Debounced highlighting
|
|
252
|
+
function debouncedHighlight(): void {
|
|
253
|
+
if (highlightTimeout) {
|
|
254
|
+
clearTimeout(highlightTimeout);
|
|
255
|
+
}
|
|
256
|
+
highlightTimeout = setTimeout(() => {
|
|
257
|
+
if (!codeRef.value || !isEditing.value) return;
|
|
258
|
+
|
|
259
|
+
// Save cursor position
|
|
260
|
+
const cursorOffset = getCursorOffset(codeRef.value);
|
|
261
|
+
|
|
262
|
+
// Re-apply highlighting
|
|
263
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
264
|
+
|
|
265
|
+
// Restore cursor position
|
|
266
|
+
setCursorOffset(codeRef.value, cursorOffset);
|
|
267
|
+
}, 300);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Toggle edit mode
|
|
271
|
+
function toggleEdit(): void {
|
|
272
|
+
internalEditable.value = !internalEditable.value;
|
|
273
|
+
onEmitEditable(internalEditable.value);
|
|
274
|
+
|
|
275
|
+
if (internalEditable.value) {
|
|
276
|
+
// Entering edit mode - initialize editing content and clear any previous error
|
|
277
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
278
|
+
validationError.value = null;
|
|
279
|
+
// Set content imperatively with syntax highlighting and focus
|
|
280
|
+
nextTick(() => {
|
|
281
|
+
if (codeRef.value) {
|
|
282
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
283
|
+
codeRef.value.focus();
|
|
284
|
+
// Move cursor to end
|
|
285
|
+
const selection = window.getSelection();
|
|
286
|
+
const range = document.createRange();
|
|
287
|
+
range.selectNodeContents(codeRef.value);
|
|
288
|
+
range.collapse(false);
|
|
289
|
+
selection?.removeAllRanges();
|
|
290
|
+
selection?.addRange(range);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Exiting edit mode - clear validation error
|
|
295
|
+
validationError.value = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Handle contenteditable input
|
|
300
|
+
function onContentEditableInput(event: Event): void {
|
|
301
|
+
if (!isEditing.value) return;
|
|
302
|
+
|
|
303
|
+
isUserEditing.value = true;
|
|
304
|
+
const target = event.target as HTMLElement;
|
|
305
|
+
editingContent.value = target.innerText || "";
|
|
306
|
+
|
|
307
|
+
debouncedValidate();
|
|
308
|
+
debouncedHighlight();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Handle blur - emit changes and re-apply highlighting
|
|
312
|
+
function onContentEditableBlur(): void {
|
|
313
|
+
if (!isEditing.value || !isUserEditing.value) return;
|
|
314
|
+
|
|
315
|
+
isUserEditing.value = false;
|
|
316
|
+
|
|
317
|
+
// Clear pending timeouts and process immediately
|
|
318
|
+
if (validationTimeout) {
|
|
319
|
+
clearTimeout(validationTimeout);
|
|
320
|
+
validationTimeout = null;
|
|
321
|
+
}
|
|
322
|
+
if (highlightTimeout) {
|
|
323
|
+
clearTimeout(highlightTimeout);
|
|
324
|
+
highlightTimeout = null;
|
|
325
|
+
}
|
|
326
|
+
validationError.value = codeFormat.validateWithError(editingContent.value, currentFormat.value);
|
|
327
|
+
|
|
328
|
+
// Parse and emit the value
|
|
329
|
+
const parsed = codeFormat.parse(editingContent.value);
|
|
330
|
+
if (parsed) {
|
|
331
|
+
onEmitModelValue(parsed);
|
|
332
|
+
} else {
|
|
333
|
+
onEmitModelValue(editingContent.value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Re-apply syntax highlighting after editing
|
|
337
|
+
if (codeRef.value) {
|
|
338
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle keyboard shortcuts in edit mode
|
|
343
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
344
|
+
if (!isEditing.value) return;
|
|
345
|
+
|
|
346
|
+
// Enter key - smart indentation
|
|
347
|
+
if (event.key === "Enter") {
|
|
348
|
+
const lineInfo = getCurrentLineInfo(editingContent.value, codeRef.value);
|
|
349
|
+
if (lineInfo) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
const smartIndent = getSmartIndent(lineInfo, currentFormat.value);
|
|
352
|
+
|
|
353
|
+
const selection = window.getSelection();
|
|
354
|
+
if (selection && selection.rangeCount > 0) {
|
|
355
|
+
const range = selection.getRangeAt(0);
|
|
356
|
+
range.deleteContents();
|
|
357
|
+
const textNode = document.createTextNode("\n" + smartIndent);
|
|
358
|
+
range.insertNode(textNode);
|
|
359
|
+
range.setStartAfter(textNode);
|
|
360
|
+
range.setEndAfter(textNode);
|
|
361
|
+
selection.removeAllRanges();
|
|
362
|
+
selection.addRange(range);
|
|
363
|
+
|
|
364
|
+
codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Tab key - insert spaces instead of moving focus
|
|
370
|
+
if (event.key === "Tab") {
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
document.execCommand("insertText", false, " ");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Escape - exit edit mode
|
|
376
|
+
if (event.key === "Escape") {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
onContentEditableBlur();
|
|
379
|
+
toggleEdit();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Ctrl/Cmd + S - save without exiting
|
|
383
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
onContentEditableBlur();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Cleanup timeouts on unmount
|
|
390
|
+
onUnmounted(() => {
|
|
391
|
+
if (validationTimeout) clearTimeout(validationTimeout);
|
|
392
|
+
if (highlightTimeout) clearTimeout(highlightTimeout);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
// State
|
|
397
|
+
internalEditable,
|
|
398
|
+
editingContent,
|
|
399
|
+
cachedHighlightedContent,
|
|
400
|
+
isUserEditing,
|
|
401
|
+
validationError,
|
|
402
|
+
|
|
403
|
+
// Computed
|
|
404
|
+
isEditing,
|
|
405
|
+
hasValidationError,
|
|
406
|
+
highlightedContent,
|
|
407
|
+
displayContent,
|
|
408
|
+
charCount,
|
|
409
|
+
isValid,
|
|
410
|
+
|
|
411
|
+
// Methods
|
|
412
|
+
toggleEdit,
|
|
413
|
+
onContentEditableInput,
|
|
414
|
+
onContentEditableBlur,
|
|
415
|
+
onKeyDown,
|
|
416
|
+
syncEditableFromProp,
|
|
417
|
+
syncEditingContentFromValue,
|
|
418
|
+
updateEditingContentOnFormatChange
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { computed, ComputedRef, Ref } from "vue";
|
|
2
|
+
import * as fileHelpers from "../helpers/filePreviewHelpers";
|
|
3
|
+
import { getMimeType, getOptimizedUrl, isExternalLinkFile } from "../helpers/filePreviewHelpers";
|
|
4
|
+
import { UploadedFile } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface FileTranscode {
|
|
7
|
+
status: "Complete" | "Pending" | "In Progress" | "Timeout";
|
|
8
|
+
progress: number;
|
|
9
|
+
estimate_ms: number;
|
|
10
|
+
started_at: string;
|
|
11
|
+
completed_at: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseFilePreviewOptions {
|
|
16
|
+
file: Ref<UploadedFile | null | undefined>;
|
|
17
|
+
src?: Ref<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseFilePreviewReturn {
|
|
21
|
+
computedImage: ComputedRef<UploadedFile | null>;
|
|
22
|
+
filename: ComputedRef<string>;
|
|
23
|
+
mimeType: ComputedRef<string>;
|
|
24
|
+
isImage: ComputedRef<boolean>;
|
|
25
|
+
isVideo: ComputedRef<boolean>;
|
|
26
|
+
isPdf: ComputedRef<boolean>;
|
|
27
|
+
isExternalLink: ComputedRef<boolean>;
|
|
28
|
+
previewUrl: ComputedRef<string>;
|
|
29
|
+
thumbUrl: ComputedRef<string>;
|
|
30
|
+
isPreviewable: ComputedRef<boolean>;
|
|
31
|
+
hasMetadata: ComputedRef<boolean>;
|
|
32
|
+
metadataKeyCount: ComputedRef<number>;
|
|
33
|
+
filteredMetadata: ComputedRef<Record<string, unknown>>;
|
|
34
|
+
hasTranscodes: ComputedRef<boolean>;
|
|
35
|
+
transcodingStatus: ComputedRef<(FileTranscode & { message: string }) | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Composable for file preview computed properties
|
|
40
|
+
*/
|
|
41
|
+
export function useFilePreview(options: UseFilePreviewOptions): UseFilePreviewReturn {
|
|
42
|
+
const { file, src } = options;
|
|
43
|
+
|
|
44
|
+
const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
|
|
45
|
+
if (file.value) {
|
|
46
|
+
return file.value;
|
|
47
|
+
} else if (src?.value) {
|
|
48
|
+
return {
|
|
49
|
+
id: src.value,
|
|
50
|
+
url: src.value,
|
|
51
|
+
type: "image/" + src.value.split(".").pop()?.toLowerCase(),
|
|
52
|
+
name: "",
|
|
53
|
+
size: 0,
|
|
54
|
+
__type: "BrowserFile"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
|
61
|
+
const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
|
|
62
|
+
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
63
|
+
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
64
|
+
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
65
|
+
const isExternalLink = computed(() => computedImage.value ? isExternalLinkFile(computedImage.value) : false);
|
|
66
|
+
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
67
|
+
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
68
|
+
const isPreviewable = computed(() => !!thumbUrl.value || isVideo.value || isImage.value);
|
|
69
|
+
|
|
70
|
+
const hasMetadata = computed(() => {
|
|
71
|
+
if (!file.value?.meta) return false;
|
|
72
|
+
const metaKeys = Object.keys(file.value.meta).filter(k => k !== "transcodes");
|
|
73
|
+
return metaKeys.length > 0;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const metadataKeyCount = computed(() => {
|
|
77
|
+
if (!file.value?.meta) return 0;
|
|
78
|
+
return Object.keys(file.value.meta).filter(k => k !== "transcodes").length;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const filteredMetadata = computed(() => {
|
|
82
|
+
if (!file.value?.meta) return {};
|
|
83
|
+
const { transcodes, ...rest } = file.value.meta;
|
|
84
|
+
return rest;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const hasTranscodes = computed(() => (file.value?.transcodes?.length || 0) > 0);
|
|
88
|
+
|
|
89
|
+
const transcodingStatus = computed(() => {
|
|
90
|
+
const metaTranscodes: Record<string, FileTranscode> = file.value?.meta?.transcodes || {};
|
|
91
|
+
|
|
92
|
+
for (const transcodeName of Object.keys(metaTranscodes)) {
|
|
93
|
+
const transcode = metaTranscodes[transcodeName];
|
|
94
|
+
if (!["Complete", "Timeout"].includes(transcode?.status)) {
|
|
95
|
+
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
computedImage,
|
|
104
|
+
filename,
|
|
105
|
+
mimeType,
|
|
106
|
+
isImage,
|
|
107
|
+
isVideo,
|
|
108
|
+
isPdf,
|
|
109
|
+
isExternalLink,
|
|
110
|
+
previewUrl,
|
|
111
|
+
thumbUrl,
|
|
112
|
+
isPreviewable,
|
|
113
|
+
hasMetadata,
|
|
114
|
+
metadataKeyCount,
|
|
115
|
+
filteredMetadata,
|
|
116
|
+
hasTranscodes,
|
|
117
|
+
transcodingStatus
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { onMounted, Ref, ref, watch } from "vue";
|
|
2
|
+
import { danxOptions } from "../config";
|
|
3
|
+
import { UploadedFile } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface UseTranscodeLoaderOptions {
|
|
6
|
+
file: Ref<UploadedFile | null | undefined>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseTranscodeLoaderReturn {
|
|
10
|
+
isLoading: Ref<boolean>;
|
|
11
|
+
loadTranscodes: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Composable for loading transcodes for a file
|
|
16
|
+
* Automatically loads transcodes on mount and when the file changes
|
|
17
|
+
*/
|
|
18
|
+
export function useTranscodeLoader(options: UseTranscodeLoaderOptions): UseTranscodeLoaderReturn {
|
|
19
|
+
const { file } = options;
|
|
20
|
+
const isLoading = ref(false);
|
|
21
|
+
|
|
22
|
+
function shouldLoadTranscodes(): boolean {
|
|
23
|
+
if (!file.value?.id) return false;
|
|
24
|
+
if (isLoading.value) return false;
|
|
25
|
+
if (!danxOptions.value.fileUpload?.refreshFile) return false;
|
|
26
|
+
|
|
27
|
+
// Only load if transcodes is explicitly null, undefined, or an empty array
|
|
28
|
+
const transcodes = file.value.transcodes;
|
|
29
|
+
return transcodes === null || transcodes === undefined || (Array.isArray(transcodes) && transcodes.length === 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadTranscodes() {
|
|
33
|
+
if (!shouldLoadTranscodes()) return;
|
|
34
|
+
|
|
35
|
+
isLoading.value = true;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const refreshFile = danxOptions.value.fileUpload?.refreshFile;
|
|
39
|
+
if (refreshFile && file.value?.id) {
|
|
40
|
+
const refreshedFile = await refreshFile(file.value.id);
|
|
41
|
+
|
|
42
|
+
// Update the file object with the loaded transcodes
|
|
43
|
+
if (refreshedFile.transcodes && file.value) {
|
|
44
|
+
file.value.transcodes = refreshedFile.transcodes;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Failed to load transcodes:", error);
|
|
49
|
+
} finally {
|
|
50
|
+
isLoading.value = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load transcodes when component mounts
|
|
55
|
+
onMounted(() => {
|
|
56
|
+
loadTranscodes();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Watch for file changes and reload transcodes if needed
|
|
60
|
+
watch(() => file.value?.id, () => {
|
|
61
|
+
loadTranscodes();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
isLoading,
|
|
66
|
+
loadTranscodes
|
|
67
|
+
};
|
|
68
|
+
}
|