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.
- package/dist/danx.es.js +24432 -22819
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +130 -119
- 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 +124 -162
- 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/filePreviewHelpers.ts +31 -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/helpers/objectStore.ts +10 -2
- 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
- package/src/svg/GoogleDocsIcon.vue +88 -0
- package/src/svg/index.ts +1 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dx-markdown-content">
|
|
3
|
+
<template v-for="(token, index) in tokens" :key="index">
|
|
4
|
+
<!-- Headings -->
|
|
5
|
+
<component
|
|
6
|
+
v-if="token.type === 'heading'"
|
|
7
|
+
:is="'h' + token.level"
|
|
8
|
+
v-html="parseInlineContent(token.content)"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<!-- Code blocks with syntax highlighting -->
|
|
12
|
+
<div
|
|
13
|
+
v-else-if="token.type === 'code_block'"
|
|
14
|
+
class="dx-markdown-code-block"
|
|
15
|
+
>
|
|
16
|
+
<!-- Language toggle badge (only for json/yaml) -->
|
|
17
|
+
<LanguageBadge
|
|
18
|
+
v-if="isToggleableLanguage(token.language)"
|
|
19
|
+
:format="getCodeBlockFormat(index, token.language)"
|
|
20
|
+
:available-formats="['json', 'yaml']"
|
|
21
|
+
:toggleable="true"
|
|
22
|
+
@change="(fmt) => setCodeBlockFormat(index, fmt)"
|
|
23
|
+
/>
|
|
24
|
+
<LanguageBadge
|
|
25
|
+
v-else-if="token.language"
|
|
26
|
+
:format="token.language"
|
|
27
|
+
:available-formats="[]"
|
|
28
|
+
:toggleable="false"
|
|
29
|
+
/>
|
|
30
|
+
<pre><code
|
|
31
|
+
:class="'language-' + getCodeBlockFormat(index, token.language)"
|
|
32
|
+
v-html="highlightCodeBlock(index, token.content, token.language)"
|
|
33
|
+
></code></pre>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Blockquotes (recursive) -->
|
|
37
|
+
<blockquote
|
|
38
|
+
v-else-if="token.type === 'blockquote'"
|
|
39
|
+
v-html="renderBlockquote(token.content)"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<!-- Unordered lists -->
|
|
43
|
+
<ul v-else-if="token.type === 'ul'">
|
|
44
|
+
<li
|
|
45
|
+
v-for="(item, itemIndex) in token.items"
|
|
46
|
+
:key="itemIndex"
|
|
47
|
+
v-html="parseInlineContent(item)"
|
|
48
|
+
/>
|
|
49
|
+
</ul>
|
|
50
|
+
|
|
51
|
+
<!-- Ordered lists -->
|
|
52
|
+
<ol
|
|
53
|
+
v-else-if="token.type === 'ol'"
|
|
54
|
+
:start="token.start"
|
|
55
|
+
>
|
|
56
|
+
<li
|
|
57
|
+
v-for="(item, itemIndex) in token.items"
|
|
58
|
+
:key="itemIndex"
|
|
59
|
+
v-html="parseInlineContent(item)"
|
|
60
|
+
/>
|
|
61
|
+
</ol>
|
|
62
|
+
|
|
63
|
+
<!-- Horizontal rules -->
|
|
64
|
+
<hr v-else-if="token.type === 'hr'" />
|
|
65
|
+
|
|
66
|
+
<!-- Paragraphs -->
|
|
67
|
+
<p
|
|
68
|
+
v-else-if="token.type === 'paragraph'"
|
|
69
|
+
v-html="parseInlineContent(token.content).replace(/\n/g, '<br />')"
|
|
70
|
+
/>
|
|
71
|
+
</template>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
import { computed, reactive } from "vue";
|
|
77
|
+
import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
|
|
78
|
+
import { tokenizeBlocks, parseInline, renderMarkdown, BlockToken } from "../../../helpers/formats/renderMarkdown";
|
|
79
|
+
import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
|
|
80
|
+
import LanguageBadge from "./LanguageBadge.vue";
|
|
81
|
+
|
|
82
|
+
export interface MarkdownContentProps {
|
|
83
|
+
content: string;
|
|
84
|
+
defaultCodeFormat?: "json" | "yaml";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const props = withDefaults(defineProps<MarkdownContentProps>(), {
|
|
88
|
+
content: ""
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Track format overrides for each code block (for toggling json<->yaml)
|
|
92
|
+
const codeBlockFormats = reactive<Record<number, string>>({});
|
|
93
|
+
// Cache converted content for each code block
|
|
94
|
+
const convertedContent = reactive<Record<number, string>>({});
|
|
95
|
+
|
|
96
|
+
// Tokenize the markdown content
|
|
97
|
+
const tokens = computed<BlockToken[]>(() => {
|
|
98
|
+
if (!props.content) return [];
|
|
99
|
+
return tokenizeBlocks(props.content);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Check if a language is toggleable (json or yaml)
|
|
103
|
+
function isToggleableLanguage(language: string): boolean {
|
|
104
|
+
if (!language) return false;
|
|
105
|
+
const lang = language.toLowerCase();
|
|
106
|
+
return lang === "json" || lang === "yaml";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get the current format for a code block (respecting user toggle, then default override, then original)
|
|
110
|
+
function getCodeBlockFormat(index: number, originalLanguage: string): string {
|
|
111
|
+
// If user has toggled this block, use their choice
|
|
112
|
+
if (codeBlockFormats[index]) {
|
|
113
|
+
return codeBlockFormats[index];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If a default is set and this is a toggleable language, use the default
|
|
117
|
+
const lang = originalLanguage?.toLowerCase();
|
|
118
|
+
if (props.defaultCodeFormat && (lang === "json" || lang === "yaml")) {
|
|
119
|
+
return props.defaultCodeFormat;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Otherwise use the original language
|
|
123
|
+
return lang || "text";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get converted content for a code block (handles initial conversion for defaultCodeFormat)
|
|
127
|
+
function getConvertedContent(index: number, originalContent: string, originalLang: string): string {
|
|
128
|
+
const format = getCodeBlockFormat(index, originalLang);
|
|
129
|
+
|
|
130
|
+
// If format matches original, no conversion needed
|
|
131
|
+
if (format === originalLang || !isToggleableLanguage(originalLang)) {
|
|
132
|
+
return originalContent;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Convert from original to target format
|
|
136
|
+
try {
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
|
|
139
|
+
if (originalLang === "json") {
|
|
140
|
+
parsed = JSON.parse(originalContent);
|
|
141
|
+
} else if (originalLang === "yaml") {
|
|
142
|
+
parsed = parseYAML(originalContent);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (parsed !== undefined) {
|
|
146
|
+
if (format === "json") {
|
|
147
|
+
return JSON.stringify(parsed, null, 2);
|
|
148
|
+
} else if (format === "yaml") {
|
|
149
|
+
return stringifyYAML(parsed as object);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Conversion failed, return original
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return originalContent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Set format for a code block (converts content to the new format)
|
|
160
|
+
function setCodeBlockFormat(index: number, newFormat: string) {
|
|
161
|
+
const token = tokens.value[index];
|
|
162
|
+
if (token?.type !== "code_block") return;
|
|
163
|
+
|
|
164
|
+
const originalLang = token.language?.toLowerCase() || "json";
|
|
165
|
+
const current = getCodeBlockFormat(index, originalLang);
|
|
166
|
+
|
|
167
|
+
// No change needed if already in target format
|
|
168
|
+
if (current === newFormat) return;
|
|
169
|
+
|
|
170
|
+
// Convert the content
|
|
171
|
+
try {
|
|
172
|
+
// Use the currently displayed content (which may already be converted due to defaultCodeFormat)
|
|
173
|
+
const sourceContent = convertedContent[index] || getConvertedContent(index, token.content, originalLang);
|
|
174
|
+
let parsed: unknown;
|
|
175
|
+
|
|
176
|
+
// Parse from current format
|
|
177
|
+
if (current === "json") {
|
|
178
|
+
parsed = JSON.parse(sourceContent);
|
|
179
|
+
} else {
|
|
180
|
+
parsed = parseYAML(sourceContent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Convert to new format
|
|
184
|
+
if (newFormat === "json") {
|
|
185
|
+
convertedContent[index] = JSON.stringify(parsed, null, 2);
|
|
186
|
+
} else {
|
|
187
|
+
convertedContent[index] = stringifyYAML(parsed as object);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
codeBlockFormats[index] = newFormat;
|
|
191
|
+
} catch {
|
|
192
|
+
// If conversion fails, just set the format without converting
|
|
193
|
+
codeBlockFormats[index] = newFormat;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Highlight code block content based on format
|
|
198
|
+
function highlightCodeBlock(index: number, originalContent: string, originalLanguage: string): string {
|
|
199
|
+
const format = getCodeBlockFormat(index, originalLanguage);
|
|
200
|
+
const originalLang = originalLanguage?.toLowerCase() || "text";
|
|
201
|
+
|
|
202
|
+
// Get the content (converted if needed, or from cache if user toggled)
|
|
203
|
+
const content = convertedContent[index] || getConvertedContent(index, originalContent, originalLang);
|
|
204
|
+
|
|
205
|
+
// Apply syntax highlighting
|
|
206
|
+
switch (format) {
|
|
207
|
+
case "json":
|
|
208
|
+
return highlightJSON(content);
|
|
209
|
+
case "yaml":
|
|
210
|
+
return highlightYAML(content);
|
|
211
|
+
default:
|
|
212
|
+
// For other languages, just escape HTML
|
|
213
|
+
return content
|
|
214
|
+
.replace(/&/g, "&")
|
|
215
|
+
.replace(/</g, "<")
|
|
216
|
+
.replace(/>/g, ">");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Parse inline markdown (bold, italic, links, etc.)
|
|
221
|
+
function parseInlineContent(text: string): string {
|
|
222
|
+
return parseInline(text, true);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Render blockquote content (can contain nested markdown)
|
|
226
|
+
function renderBlockquote(content: string): string {
|
|
227
|
+
return renderMarkdown(content);
|
|
228
|
+
}
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<style lang="scss">
|
|
232
|
+
.dx-markdown-code-block {
|
|
233
|
+
position: relative;
|
|
234
|
+
margin: 1em 0;
|
|
235
|
+
|
|
236
|
+
pre {
|
|
237
|
+
margin: 0;
|
|
238
|
+
background: rgba(0, 0, 0, 0.3);
|
|
239
|
+
padding: 1em;
|
|
240
|
+
border-radius: 6px;
|
|
241
|
+
overflow-x: auto;
|
|
242
|
+
|
|
243
|
+
code {
|
|
244
|
+
background: transparent;
|
|
245
|
+
padding: 0;
|
|
246
|
+
font-size: 0.875em;
|
|
247
|
+
font-family: 'Fira Code', 'Monaco', monospace;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as CodeViewer } from "./CodeViewer.vue";
|
|
2
|
+
export { default as CodeViewerCollapsed } from "./CodeViewerCollapsed.vue";
|
|
3
|
+
export { default as CodeViewerFooter } from "./CodeViewerFooter.vue";
|
|
4
|
+
export { default as LanguageBadge } from "./LanguageBadge.vue";
|
|
5
|
+
export { default as MarkdownContent } from "./MarkdownContent.vue";
|
|
@@ -10,58 +10,105 @@
|
|
|
10
10
|
<!-- Header with filename and navigation -->
|
|
11
11
|
<CarouselHeader
|
|
12
12
|
v-if="currentFile"
|
|
13
|
-
:filename="
|
|
13
|
+
:filename="filename"
|
|
14
14
|
:show-back-button="hasParent"
|
|
15
|
+
:show-metadata-button="hasMetadata"
|
|
16
|
+
:metadata-count="metadataKeyCount"
|
|
15
17
|
:show-transcodes-button="!!(currentFile.transcodes && currentFile.transcodes.length > 0)"
|
|
16
18
|
:transcodes-count="currentFile.transcodes?.length || 0"
|
|
17
19
|
@back="navigateToParent"
|
|
20
|
+
@metadata="onMetadataClick"
|
|
18
21
|
@transcodes="showTranscodeNav = true"
|
|
19
22
|
/>
|
|
20
23
|
|
|
21
|
-
<!--
|
|
22
|
-
<div class="flex-grow relative">
|
|
23
|
-
|
|
24
|
+
<!-- Content Area with optional split panel -->
|
|
25
|
+
<div class="flex-grow flex relative min-h-0">
|
|
26
|
+
<!-- Carousel Section -->
|
|
27
|
+
<div class="flex-grow relative">
|
|
28
|
+
<div class="absolute inset-0">
|
|
29
|
+
<div
|
|
30
|
+
v-for="slide in visibleSlides"
|
|
31
|
+
:key="slide.file.id"
|
|
32
|
+
:class="[
|
|
33
|
+
'absolute inset-0 flex items-center justify-center transition-opacity duration-300',
|
|
34
|
+
slide.isActive ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
|
35
|
+
]"
|
|
36
|
+
>
|
|
37
|
+
<FileRenderer
|
|
38
|
+
:file="slide.file"
|
|
39
|
+
:autoplay="slide.isActive"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Navigation Arrows -->
|
|
24
45
|
<div
|
|
25
|
-
v-
|
|
26
|
-
|
|
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
|
-
]"
|
|
46
|
+
v-if="canNavigatePrevious"
|
|
47
|
+
class="absolute left-4 top-1/2 -translate-y-1/2 z-20"
|
|
31
48
|
>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
<QBtn
|
|
50
|
+
round
|
|
51
|
+
size="lg"
|
|
52
|
+
class="bg-slate-800 text-white opacity-70 hover:opacity-100"
|
|
53
|
+
@click="navigatePrevious"
|
|
54
|
+
>
|
|
55
|
+
<ChevronLeftIcon class="w-8" />
|
|
56
|
+
</QBtn>
|
|
36
57
|
</div>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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"
|
|
58
|
+
<div
|
|
59
|
+
v-if="canNavigateNext"
|
|
60
|
+
class="absolute right-4 top-1/2 -translate-y-1/2 z-20"
|
|
49
61
|
>
|
|
50
|
-
<
|
|
51
|
-
|
|
62
|
+
<QBtn
|
|
63
|
+
round
|
|
64
|
+
size="lg"
|
|
65
|
+
class="bg-slate-800 text-white opacity-70 hover:opacity-100"
|
|
66
|
+
@click="navigateNext"
|
|
67
|
+
>
|
|
68
|
+
<ChevronRightIcon class="w-8" />
|
|
69
|
+
</QBtn>
|
|
70
|
+
</div>
|
|
52
71
|
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Split Metadata Panel -->
|
|
53
74
|
<div
|
|
54
|
-
v-if="
|
|
55
|
-
class="
|
|
75
|
+
v-if="metadataSplitMode && showMetadataDialog && hasMetadata"
|
|
76
|
+
class="w-[40%] max-w-[600px] min-w-[300px] bg-slate-900 border-l border-slate-700 flex flex-col"
|
|
56
77
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
<div class="px-4 py-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between flex-shrink-0">
|
|
79
|
+
<h3 class="text-slate-200 font-medium">Metadata</h3>
|
|
80
|
+
<div class="flex items-center gap-1">
|
|
81
|
+
<QBtn
|
|
82
|
+
flat
|
|
83
|
+
dense
|
|
84
|
+
round
|
|
85
|
+
size="sm"
|
|
86
|
+
class="text-slate-400 hover:text-white hover:bg-slate-700"
|
|
87
|
+
@click="undockToModal"
|
|
88
|
+
>
|
|
89
|
+
<UndockIcon class="w-4 h-4" />
|
|
90
|
+
<QTooltip>Undock to modal</QTooltip>
|
|
91
|
+
</QBtn>
|
|
92
|
+
<QBtn
|
|
93
|
+
flat
|
|
94
|
+
dense
|
|
95
|
+
round
|
|
96
|
+
size="sm"
|
|
97
|
+
class="text-slate-400 hover:text-white hover:bg-slate-700"
|
|
98
|
+
@click="showMetadataDialog = false"
|
|
99
|
+
>
|
|
100
|
+
<CloseIcon class="w-4 h-4" />
|
|
101
|
+
<QTooltip>Close</QTooltip>
|
|
102
|
+
</QBtn>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex-1 min-h-0 p-4">
|
|
106
|
+
<CodeViewer
|
|
107
|
+
:model-value="filteredMetadata"
|
|
108
|
+
:readonly="true"
|
|
109
|
+
format="yaml"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
65
112
|
</div>
|
|
66
113
|
</div>
|
|
67
114
|
|
|
@@ -88,19 +135,35 @@
|
|
|
88
135
|
:transcodes="currentFile.transcodes"
|
|
89
136
|
@select="onSelectTranscode"
|
|
90
137
|
/>
|
|
138
|
+
|
|
139
|
+
<!-- Metadata Dialog (only in modal mode) -->
|
|
140
|
+
<FileMetadataDialog
|
|
141
|
+
v-if="showMetadataDialog && !metadataSplitMode"
|
|
142
|
+
:filename="filename"
|
|
143
|
+
:mime-type="mimeType"
|
|
144
|
+
:metadata="filteredMetadata"
|
|
145
|
+
:show-dock-button="true"
|
|
146
|
+
@close="showMetadataDialog = false"
|
|
147
|
+
@dock="dockToSplit"
|
|
148
|
+
/>
|
|
91
149
|
</div>
|
|
92
150
|
</QDialog>
|
|
93
151
|
</template>
|
|
94
152
|
|
|
95
153
|
<script setup lang="ts">
|
|
96
154
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/outline";
|
|
155
|
+
import { FaSolidWindowMaximize as UndockIcon } from "danx-icon";
|
|
97
156
|
import { ref } from "vue";
|
|
98
157
|
import { useFileNavigation } from "../../../composables/useFileNavigation";
|
|
158
|
+
import { useFilePreview } from "../../../composables/useFilePreview";
|
|
99
159
|
import { useKeyboardNavigation } from "../../../composables/useKeyboardNavigation";
|
|
100
160
|
import { useVirtualCarousel } from "../../../composables/useVirtualCarousel";
|
|
161
|
+
import { getItem, setItem } from "../../../helpers";
|
|
101
162
|
import { XIcon as CloseIcon } from "../../../svg";
|
|
102
163
|
import { UploadedFile } from "../../../types";
|
|
164
|
+
import { CodeViewer } from "../Code";
|
|
103
165
|
import CarouselHeader from "../Files/CarouselHeader.vue";
|
|
166
|
+
import FileMetadataDialog from "../Files/FileMetadataDialog.vue";
|
|
104
167
|
import FileRenderer from "../Files/FileRenderer.vue";
|
|
105
168
|
import ThumbnailStrip from "../Files/ThumbnailStrip.vue";
|
|
106
169
|
import TranscodeNavigator from "../Files/TranscodeNavigator.vue";
|
|
@@ -155,4 +218,37 @@ function onSelectTranscode(transcode: UploadedFile, index: number) {
|
|
|
155
218
|
diveInto(transcode, currentFile.value.transcodes);
|
|
156
219
|
}
|
|
157
220
|
}
|
|
221
|
+
|
|
222
|
+
// Metadata navigation
|
|
223
|
+
const showMetadataDialog = ref(false);
|
|
224
|
+
const METADATA_MODE_KEY = "danx-file-preview-metadata-mode";
|
|
225
|
+
const metadataSplitMode = ref(getItem(METADATA_MODE_KEY, false));
|
|
226
|
+
|
|
227
|
+
function onMetadataClick() {
|
|
228
|
+
// Toggle metadata visibility - mode determines whether it shows as split or modal
|
|
229
|
+
showMetadataDialog.value = !showMetadataDialog.value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function dockToSplit() {
|
|
233
|
+
// Switch from modal to split view
|
|
234
|
+
metadataSplitMode.value = true;
|
|
235
|
+
setItem(METADATA_MODE_KEY, true);
|
|
236
|
+
// Keep showMetadataDialog true so it shows in split mode
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function undockToModal() {
|
|
240
|
+
// Switch from split to modal view
|
|
241
|
+
metadataSplitMode.value = false;
|
|
242
|
+
setItem(METADATA_MODE_KEY, false);
|
|
243
|
+
// Keep showMetadataDialog true so it shows as modal
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Use file preview composable for metadata
|
|
247
|
+
const {
|
|
248
|
+
filename,
|
|
249
|
+
mimeType,
|
|
250
|
+
hasMetadata,
|
|
251
|
+
metadataKeyCount,
|
|
252
|
+
filteredMetadata
|
|
253
|
+
} = useFilePreview({ file: currentFile });
|
|
158
254
|
</script>
|
|
@@ -22,6 +22,24 @@
|
|
|
22
22
|
{{ filename }}
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
+
<!-- Metadata Button -->
|
|
26
|
+
<QBtn
|
|
27
|
+
v-if="showMetadataButton"
|
|
28
|
+
flat
|
|
29
|
+
dense
|
|
30
|
+
class="bg-purple-700 text-purple-200 hover:bg-purple-600"
|
|
31
|
+
@click="$emit('metadata')"
|
|
32
|
+
>
|
|
33
|
+
<div class="flex items-center flex-nowrap gap-1">
|
|
34
|
+
<MetaIcon class="w-4" />
|
|
35
|
+
<QBadge
|
|
36
|
+
class="bg-purple-900 text-purple-200"
|
|
37
|
+
:label="metadataCount"
|
|
38
|
+
/>
|
|
39
|
+
<span class="text-sm ml-1">Metadata</span>
|
|
40
|
+
</div>
|
|
41
|
+
</QBtn>
|
|
42
|
+
|
|
25
43
|
<!-- Transcodes Button -->
|
|
26
44
|
<QBtn
|
|
27
45
|
v-if="showTranscodesButton"
|
|
@@ -57,16 +75,21 @@
|
|
|
57
75
|
|
|
58
76
|
<script setup lang="ts">
|
|
59
77
|
import { ArrowLeftIcon, FilmIcon } from "@heroicons/vue/outline";
|
|
78
|
+
import { FaSolidBarcode as MetaIcon } from "danx-icon";
|
|
60
79
|
import { XIcon as CloseIcon } from "../../../svg";
|
|
61
80
|
|
|
62
81
|
withDefaults(defineProps<{
|
|
63
82
|
filename: string;
|
|
64
83
|
showBackButton?: boolean;
|
|
84
|
+
showMetadataButton?: boolean;
|
|
85
|
+
metadataCount?: number;
|
|
65
86
|
showTranscodesButton?: boolean;
|
|
66
87
|
transcodesCount?: number;
|
|
67
88
|
showCloseButton?: boolean;
|
|
68
89
|
}>(), {
|
|
69
90
|
showBackButton: false,
|
|
91
|
+
showMetadataButton: false,
|
|
92
|
+
metadataCount: 0,
|
|
70
93
|
showTranscodesButton: false,
|
|
71
94
|
transcodesCount: 0,
|
|
72
95
|
showCloseButton: false
|
|
@@ -74,6 +97,7 @@ withDefaults(defineProps<{
|
|
|
74
97
|
|
|
75
98
|
defineEmits<{
|
|
76
99
|
'back': [];
|
|
100
|
+
'metadata': [];
|
|
77
101
|
'transcodes': [];
|
|
78
102
|
'close': [];
|
|
79
103
|
}>();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<InfoDialog
|
|
3
|
+
title="File Metadata"
|
|
4
|
+
:hide-done="true"
|
|
5
|
+
done-text="Close"
|
|
6
|
+
content-class="w-[80vw] h-[80vh] max-w-none"
|
|
7
|
+
@close="$emit('close')"
|
|
8
|
+
>
|
|
9
|
+
<div class="file-metadata-container h-full flex flex-col">
|
|
10
|
+
<!-- File info header -->
|
|
11
|
+
<div class="bg-sky-50 rounded-lg p-4 mb-4 flex-shrink-0">
|
|
12
|
+
<h4 class="text-lg font-semibold text-gray-900 mb-2">
|
|
13
|
+
{{ filename || 'Unnamed File' }}
|
|
14
|
+
</h4>
|
|
15
|
+
<div v-if="mimeType" class="text-sm text-gray-600">
|
|
16
|
+
Type: {{ mimeType }}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Metadata section -->
|
|
21
|
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden flex-1 flex flex-col min-h-0">
|
|
22
|
+
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex-shrink-0 flex items-center justify-between">
|
|
23
|
+
<h4 class="text-base font-medium text-gray-900">
|
|
24
|
+
Metadata
|
|
25
|
+
</h4>
|
|
26
|
+
<QBtn
|
|
27
|
+
v-if="showDockButton"
|
|
28
|
+
flat
|
|
29
|
+
dense
|
|
30
|
+
round
|
|
31
|
+
size="sm"
|
|
32
|
+
class="text-gray-500 hover:text-gray-700 hover:bg-gray-200"
|
|
33
|
+
@click="$emit('dock')"
|
|
34
|
+
>
|
|
35
|
+
<DockSideIcon class="w-4 h-4" />
|
|
36
|
+
<QTooltip>Dock to side</QTooltip>
|
|
37
|
+
</QBtn>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="p-4 flex-1 min-h-0 flex flex-col">
|
|
40
|
+
<CodeViewer
|
|
41
|
+
:model-value="metadata"
|
|
42
|
+
:readonly="true"
|
|
43
|
+
format="yaml"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</InfoDialog>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
import { FaSolidTableColumns as DockSideIcon } from "danx-icon";
|
|
53
|
+
import { CodeViewer } from "../Code";
|
|
54
|
+
import { InfoDialog } from "../Dialogs";
|
|
55
|
+
|
|
56
|
+
withDefaults(defineProps<{
|
|
57
|
+
filename: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
metadata: Record<string, unknown>;
|
|
60
|
+
showDockButton?: boolean;
|
|
61
|
+
}>(), {
|
|
62
|
+
showDockButton: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
defineEmits<{
|
|
66
|
+
close: [];
|
|
67
|
+
dock: [];
|
|
68
|
+
}>();
|
|
69
|
+
</script>
|