quasar-ui-danx 0.4.94 → 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.
Files changed (37) hide show
  1. package/dist/danx.es.js +24432 -22819
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +130 -119
  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/Buttons/ActionButton.vue +11 -3
  8. package/src/components/Utility/Code/CodeViewer.vue +219 -0
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
  11. package/src/components/Utility/Code/LanguageBadge.vue +122 -0
  12. package/src/components/Utility/Code/MarkdownContent.vue +251 -0
  13. package/src/components/Utility/Code/index.ts +5 -0
  14. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
  15. package/src/components/Utility/Files/CarouselHeader.vue +24 -0
  16. package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
  17. package/src/components/Utility/Files/FilePreview.vue +124 -162
  18. package/src/components/Utility/Files/index.ts +1 -0
  19. package/src/components/Utility/index.ts +1 -0
  20. package/src/composables/index.ts +5 -0
  21. package/src/composables/useCodeFormat.ts +199 -0
  22. package/src/composables/useCodeViewerCollapse.ts +125 -0
  23. package/src/composables/useCodeViewerEditor.ts +420 -0
  24. package/src/composables/useFilePreview.ts +119 -0
  25. package/src/composables/useTranscodeLoader.ts +68 -0
  26. package/src/helpers/filePreviewHelpers.ts +31 -0
  27. package/src/helpers/formats/highlightSyntax.ts +327 -0
  28. package/src/helpers/formats/index.ts +3 -1
  29. package/src/helpers/formats/renderMarkdown.ts +338 -0
  30. package/src/helpers/objectStore.ts +10 -2
  31. package/src/styles/danx.scss +3 -0
  32. package/src/styles/themes/danx/code.scss +158 -0
  33. package/src/styles/themes/danx/index.scss +2 -0
  34. package/src/styles/themes/danx/markdown.scss +145 -0
  35. package/src/styles/themes/danx/scrollbar.scss +125 -0
  36. package/src/svg/GoogleDocsIcon.vue +88 -0
  37. package/src/svg/index.ts +1 -0
@@ -37,8 +37,12 @@
37
37
  v-else
38
38
  class="flex items-center justify-center h-full"
39
39
  >
40
+ <GoogleDocsIcon
41
+ v-if="isExternalLink"
42
+ class="h-3/4"
43
+ />
40
44
  <PdfIcon
41
- v-if="isPdf"
45
+ v-else-if="isPdf"
42
46
  class="w-3/4"
43
47
  />
44
48
  <TextFileIcon
@@ -70,7 +74,7 @@
70
74
  >
71
75
  <QLinearProgress
72
76
  :key="'progress-' + isUploading ? 'uploading' : 'transcoding'"
73
- :value="isUploading ? file.progress : ((transcodingStatus?.progress || 0) / 100)"
77
+ :value="isUploading ? (file?.progress || 0) : ((transcodingStatus?.progress || 0) / 100)"
74
78
  size="36px"
75
79
  :color="isUploading ? 'green-800' : 'blue-800'"
76
80
  :animation-speed="transcodingStatus?.estimate_ms || 3000"
@@ -103,6 +107,22 @@
103
107
  </template>
104
108
 
105
109
  <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">
110
+ <QBtn
111
+ v-if="hasMetadata"
112
+ :size="btnSize"
113
+ class="dx-file-preview-metadata bg-purple-700 text-white opacity-70 hover:opacity-100 py-1 px-2 relative"
114
+ @click.stop="showMetadataDialog = true"
115
+ >
116
+ <div class="flex items-center flex-nowrap gap-1">
117
+ <MetaIcon class="w-4 h-4" />
118
+ <QBadge
119
+ class="bg-purple-900 text-purple-200"
120
+ :label="metadataKeyCount"
121
+ />
122
+ </div>
123
+ <QTooltip>View Metadata</QTooltip>
124
+ </QBtn>
125
+
106
126
  <QBtn
107
127
  v-if="hasTranscodes"
108
128
  :size="btnSize"
@@ -148,6 +168,14 @@
148
168
  </QBtn>
149
169
  </div>
150
170
 
171
+ <FileMetadataDialog
172
+ v-if="showMetadataDialog"
173
+ :filename="filename"
174
+ :mime-type="mimeType"
175
+ :metadata="filteredMetadata"
176
+ @close="showMetadataDialog = false"
177
+ />
178
+
151
179
  <FullScreenCarouselDialog
152
180
  v-if="showPreview && !disabled && previewableFiles"
153
181
  :files="previewableFiles"
@@ -159,198 +187,132 @@
159
187
 
160
188
  <script setup lang="ts">
161
189
  import { DocumentTextIcon as TextFileIcon, DownloadIcon, FilmIcon, PlayIcon } from "@heroicons/vue/outline";
162
- import { computed, ComputedRef, onMounted, ref, watch } from "vue";
163
- import { danxOptions } from "../../../config";
190
+ import { FaSolidBarcode as MetaIcon } from "danx-icon";
191
+ import { computed, ref, toRef } from "vue";
192
+ import { useFilePreview } from "../../../composables/useFilePreview";
193
+ import { useTranscodeLoader } from "../../../composables/useTranscodeLoader";
164
194
  import { download, uniqueBy } from "../../../helpers";
165
- import * as fileHelpers from "../../../helpers/filePreviewHelpers";
166
- import { getMimeType, getOptimizedUrl } from "../../../helpers/filePreviewHelpers";
167
- import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
195
+ import { isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
196
+ import { GoogleDocsIcon, ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
168
197
  import { UploadedFile } from "../../../types";
169
198
  import { FullScreenCarouselDialog } from "../Dialogs";
170
-
171
- export interface FileTranscode {
172
- status: "Complete" | "Pending" | "In Progress";
173
- progress: number;
174
- estimate_ms: number;
175
- started_at: string;
176
- completed_at: string;
177
- message?: string;
178
- }
199
+ import FileMetadataDialog from "./FileMetadataDialog.vue";
179
200
 
180
201
  export interface FilePreviewProps {
181
- src?: string;
182
- file?: UploadedFile;
183
- relatedFiles?: UploadedFile[];
184
- missingIcon?: any;
185
- showFilename?: boolean;
186
- downloadButtonClass?: string;
187
- imageFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
188
- downloadable?: boolean;
189
- removable?: boolean;
190
- disabled?: boolean;
191
- square?: boolean;
192
- btnSize?: "xs" | "sm" | "md" | "lg";
202
+ src?: string;
203
+ file?: UploadedFile;
204
+ relatedFiles?: UploadedFile[];
205
+ missingIcon?: any;
206
+ showFilename?: boolean;
207
+ downloadButtonClass?: string;
208
+ imageFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
209
+ downloadable?: boolean;
210
+ removable?: boolean;
211
+ disabled?: boolean;
212
+ square?: boolean;
213
+ btnSize?: "xs" | "sm" | "md" | "lg";
193
214
  }
194
215
 
195
216
  const emit = defineEmits(["remove"]);
196
217
 
197
218
  const props = withDefaults(defineProps<FilePreviewProps>(), {
198
- src: "",
199
- file: null,
200
- relatedFiles: null,
201
- missingIcon: ImageIcon,
202
- downloadButtonClass: "bg-blue-600 text-white",
203
- imageFit: "cover",
204
- downloadable: false,
205
- removable: false,
206
- disabled: false,
207
- square: false,
208
- btnSize: "sm"
219
+ src: "",
220
+ file: null,
221
+ relatedFiles: null,
222
+ missingIcon: ImageIcon,
223
+ downloadButtonClass: "bg-blue-600 text-white",
224
+ imageFit: "cover",
225
+ downloadable: false,
226
+ removable: false,
227
+ disabled: false,
228
+ square: false,
229
+ btnSize: "sm"
209
230
  });
210
231
 
232
+ // Use composables for file preview logic
233
+ const fileRef = toRef(props, "file");
234
+ const srcRef = toRef(props, "src");
211
235
 
236
+ const {
237
+ computedImage,
238
+ filename,
239
+ mimeType,
240
+ isVideo,
241
+ isPdf,
242
+ isExternalLink,
243
+ previewUrl,
244
+ thumbUrl,
245
+ isPreviewable,
246
+ hasMetadata,
247
+ metadataKeyCount,
248
+ filteredMetadata,
249
+ hasTranscodes,
250
+ transcodingStatus
251
+ } = useFilePreview({ file: fileRef, src: srcRef });
252
+
253
+ // Load transcodes automatically
254
+ useTranscodeLoader({ file: fileRef });
255
+
256
+ // Local state
212
257
  const showPreview = ref(false);
213
- const isLoadingTranscodes = ref(false);
214
- const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
215
- if (props.file) {
216
- return props.file;
217
- } else if (props.src) {
218
- return {
219
- id: props.src,
220
- url: props.src,
221
- type: "image/" + props.src.split(".").pop()?.toLowerCase(),
222
- name: "",
223
- size: 0,
224
- __type: "BrowserFile"
225
- };
226
- }
227
- return null;
228
- });
258
+ const showMetadataDialog = ref(false);
259
+ const isConfirmingRemove = ref(false);
229
260
 
230
- const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
261
+ // Computed
262
+ const isUploading = computed(() => props.file && props.file?.progress !== undefined);
231
263
  const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
232
- const hasTranscodes = computed(() => (props.file?.transcodes?.length || 0) > 0);
233
- const previewableFiles: ComputedRef<(UploadedFile | null)[] | null> = computed(() => {
234
- return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl) : [computedImage.value];
264
+
265
+ const previewableFiles = computed(() => {
266
+ return props.relatedFiles?.length > 0
267
+ ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl)
268
+ : [computedImage.value];
235
269
  });
236
270
 
271
+ // Helpers
237
272
  function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
238
- return a.id === b.id ||
239
- [b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
240
- [a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
273
+ return a.id === b.id ||
274
+ [b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
275
+ [a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
241
276
  }
242
277
 
243
- const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
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 || "");
250
- const isPreviewable = computed(() => {
251
- return !!thumbUrl.value || isVideo.value || isImage.value;
252
- });
253
-
254
- /**
255
- * Resolve the active transcoding operation if there is one, otherwise return null
256
- */
257
- const transcodingStatus = computed(() => {
258
- let status = null;
259
- const metaTranscodes: FileTranscode[] = props.file?.meta?.transcodes || [];
260
-
261
- for (let transcodeName of Object.keys(metaTranscodes)) {
262
- const transcode = metaTranscodes[transcodeName];
263
- if (!["Complete", "Timeout"].includes(transcode?.status)) {
264
- return { ...transcode, message: `${transcodeName} ${transcode.status}` };
265
- }
266
- }
267
-
268
- return status;
269
- });
270
-
271
- const isConfirmingRemove = ref(false);
272
278
  function onRemove() {
273
- if (!isConfirmingRemove.value) {
274
- isConfirmingRemove.value = true;
275
- setTimeout(() => {
276
- isConfirmingRemove.value = false;
277
- }, 2000);
278
- } else {
279
- emit("remove");
280
- }
279
+ if (!isConfirmingRemove.value) {
280
+ isConfirmingRemove.value = true;
281
+ setTimeout(() => {
282
+ isConfirmingRemove.value = false;
283
+ }, 2000);
284
+ } else {
285
+ emit("remove");
286
+ }
281
287
  }
282
288
 
283
289
  function onShowPreview() {
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;
294
-
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);
290
+ if (computedImage.value && isExternalLinkFile(computedImage.value)) {
291
+ window.open(computedImage.value.url, "_blank");
292
+ return;
293
+ }
294
+ showPreview.value = true;
298
295
  }
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;
322
- }
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
- });
334
296
  </script>
335
297
 
336
298
  <style module="cls" lang="scss">
337
299
  .action-button {
338
- position: absolute;
339
- bottom: 1.5em;
340
- right: 1em;
341
- z-index: 1;
300
+ position: absolute;
301
+ bottom: 1.5em;
302
+ right: 1em;
303
+ z-index: 1;
342
304
  }
343
305
 
344
306
  .play-button {
345
- position: absolute;
346
- top: 0;
347
- left: 0;
348
- display: flex;
349
- justify-content: center;
350
- align-items: center;
351
- width: 100%;
352
- height: 100%;
353
- pointer-events: none;
354
- @apply text-blue-200;
307
+ position: absolute;
308
+ top: 0;
309
+ left: 0;
310
+ display: flex;
311
+ justify-content: center;
312
+ align-items: center;
313
+ width: 100%;
314
+ height: 100%;
315
+ pointer-events: none;
316
+ @apply text-blue-200;
355
317
  }
356
318
  </style>
@@ -1,4 +1,5 @@
1
1
  export { default as CarouselHeader } from "./CarouselHeader.vue";
2
+ export { default as FileMetadataDialog } from "./FileMetadataDialog.vue";
2
3
  export { default as FilePreview } from "./FilePreview.vue";
3
4
  export { default as FileRenderer } from "./FileRenderer.vue";
4
5
  export { default as SvgImg } from "./SvgImg.vue";
@@ -1,4 +1,5 @@
1
1
  export * from "./Buttons";
2
+ export * from "./Code";
2
3
  export * from "./Controls";
3
4
  export * from "./Dialogs";
4
5
  export * from "./Files";
@@ -1,4 +1,9 @@
1
+ export * from "./useCodeFormat";
2
+ export * from "./useCodeViewerCollapse";
3
+ export * from "./useCodeViewerEditor";
1
4
  export * from "./useFileNavigation";
5
+ export * from "./useFilePreview";
2
6
  export * from "./useKeyboardNavigation";
3
7
  export * from "./useThumbnailScroll";
8
+ export * from "./useTranscodeLoader";
4
9
  export * from "./useVirtualCarousel";
@@ -0,0 +1,199 @@
1
+ import { computed, ref, Ref } from "vue";
2
+ import { parse as parseYAML, stringify as yamlStringify } from "yaml";
3
+ import { fJSON, parseMarkdownJSON, parseMarkdownYAML } from "../helpers/formats/parsers";
4
+
5
+ export type CodeFormat = "json" | "yaml" | "text" | "markdown";
6
+
7
+ export interface UseCodeFormatOptions {
8
+ initialFormat?: CodeFormat;
9
+ initialValue?: object | string | null;
10
+ }
11
+
12
+ export interface ValidationError {
13
+ message: string;
14
+ line?: number;
15
+ column?: number;
16
+ }
17
+
18
+ export interface UseCodeFormatReturn {
19
+ // State
20
+ format: Ref<CodeFormat>;
21
+ rawContent: Ref<string>;
22
+
23
+ // Computed
24
+ parsedValue: Ref<object | null>;
25
+ formattedContent: Ref<string>;
26
+ isValid: Ref<boolean>;
27
+
28
+ // Methods
29
+ setFormat: (format: CodeFormat) => void;
30
+ setContent: (content: string) => void;
31
+ setValue: (value: object | string | null) => void;
32
+ parse: (content: string) => object | null;
33
+ formatValue: (value: object | null, targetFormat?: CodeFormat) => string;
34
+ validate: (content: string, targetFormat?: CodeFormat) => boolean;
35
+ validateWithError: (content: string, targetFormat?: CodeFormat) => ValidationError | null;
36
+ }
37
+
38
+ export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormatReturn {
39
+ const format = ref<CodeFormat>(options.initialFormat ?? "yaml");
40
+ const rawContent = ref("");
41
+
42
+ // Parse any string (JSON or YAML) to object
43
+ function parseContent(content: string): object | null {
44
+ if (!content) return null;
45
+
46
+ // Try JSON first
47
+ const jsonResult = parseMarkdownJSON(content);
48
+ if (jsonResult !== false && jsonResult !== null) return jsonResult;
49
+
50
+ // Try YAML
51
+ const yamlResult = parseMarkdownYAML(content);
52
+ if (yamlResult !== false && yamlResult !== null) return yamlResult;
53
+
54
+ return null;
55
+ }
56
+
57
+ // Format object to string in specified format
58
+ function formatValueToString(value: object | string | null, targetFormat: CodeFormat = format.value): string {
59
+ if (!value) return "";
60
+
61
+ // Text and markdown formats - just return as-is
62
+ if (targetFormat === "text" || targetFormat === "markdown") {
63
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
64
+ }
65
+
66
+ try {
67
+ const obj = typeof value === "string" ? parseContent(value) : value;
68
+ if (!obj) return typeof value === "string" ? value : "";
69
+
70
+ if (targetFormat === "json") {
71
+ const formatted = fJSON(obj);
72
+ return typeof formatted === "string" ? formatted : JSON.stringify(obj, null, 2);
73
+ } else {
74
+ return yamlStringify(obj as object);
75
+ }
76
+ } catch {
77
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
78
+ }
79
+ }
80
+
81
+ // Validate string content for a format
82
+ function validateContent(content: string, targetFormat: CodeFormat = format.value): boolean {
83
+ if (!content) return true;
84
+
85
+ // Text and markdown formats are always valid
86
+ if (targetFormat === "text" || targetFormat === "markdown") return true;
87
+
88
+ try {
89
+ if (targetFormat === "json") {
90
+ JSON.parse(content);
91
+ } else {
92
+ parseYAML(content);
93
+ }
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ // Validate and return error details if invalid
101
+ function validateContentWithError(content: string, targetFormat: CodeFormat = format.value): ValidationError | null {
102
+ if (!content) return null;
103
+
104
+ // Text and markdown formats are always valid
105
+ if (targetFormat === "text" || targetFormat === "markdown") return null;
106
+
107
+ try {
108
+ if (targetFormat === "json") {
109
+ JSON.parse(content);
110
+ } else {
111
+ parseYAML(content);
112
+ }
113
+ return null;
114
+ } catch (e: unknown) {
115
+ const error = e as Error & { linePos?: { line: number; col: number }[] };
116
+ let line: number | undefined;
117
+ let column: number | undefined;
118
+
119
+ // YAML errors from 'yaml' library have linePos
120
+ if (error.linePos && error.linePos[0]) {
121
+ line = error.linePos[0].line;
122
+ column = error.linePos[0].col;
123
+ }
124
+
125
+ // JSON parse errors - try to extract position from message
126
+ if (targetFormat === "json" && error.message) {
127
+ const posMatch = error.message.match(/position\s+(\d+)/i);
128
+ if (posMatch) {
129
+ const pos = parseInt(posMatch[1], 10);
130
+ // Convert position to line number
131
+ const lines = content.substring(0, pos).split("\n");
132
+ line = lines.length;
133
+ column = lines[lines.length - 1].length + 1;
134
+ }
135
+ }
136
+
137
+ return {
138
+ message: error.message || "Invalid syntax",
139
+ line,
140
+ column
141
+ };
142
+ }
143
+ }
144
+
145
+ // Initialize with value if provided
146
+ if (options.initialValue) {
147
+ rawContent.value = formatValueToString(options.initialValue, format.value);
148
+ }
149
+
150
+ // Computed: parsed object from raw content
151
+ const parsedValue = computed(() => parseContent(rawContent.value));
152
+
153
+ // Computed: formatted string
154
+ // For text and markdown formats, return rawContent directly without parsing
155
+ const formattedContent = computed(() => {
156
+ if (format.value === "text" || format.value === "markdown") {
157
+ return rawContent.value;
158
+ }
159
+ return formatValueToString(parsedValue.value, format.value);
160
+ });
161
+
162
+ // Computed: is current content valid
163
+ const isValid = computed(() => validateContent(rawContent.value, format.value));
164
+
165
+ // Methods
166
+ function setFormat(newFormat: CodeFormat) {
167
+ if (format.value === newFormat) return;
168
+
169
+ // Convert content to new format
170
+ const obj = parsedValue.value;
171
+ format.value = newFormat;
172
+ if (obj) {
173
+ rawContent.value = formatValueToString(obj, newFormat);
174
+ }
175
+ }
176
+
177
+ function setContent(content: string) {
178
+ rawContent.value = content;
179
+ }
180
+
181
+ function setValue(value: object | string | null) {
182
+ rawContent.value = formatValueToString(value, format.value);
183
+ }
184
+
185
+ return {
186
+ format,
187
+ rawContent,
188
+ parsedValue,
189
+ formattedContent,
190
+ isValid,
191
+ setFormat,
192
+ setContent,
193
+ setValue,
194
+ parse: parseContent,
195
+ formatValue: formatValueToString,
196
+ validate: validateContent,
197
+ validateWithError: validateContentWithError
198
+ };
199
+ }