{{ attachment.name || 'Untitled' }}
({{ formatFileSize(attachment.size) }})
+
+
+
diff --git a/src/services/mediaViewer.ts b/src/services/mediaViewer.ts
new file mode 100644
index 0000000..3472ceb
--- /dev/null
+++ b/src/services/mediaViewer.ts
@@ -0,0 +1,48 @@
+/**
+ * mediaViewer — bridges the Mail UI to the standalone media_viewer module
+ */
+
+import { defineAsyncComponent } from 'vue'
+import { useIntegrationStore } from '@KTXC'
+
+const MEDIA_VIEWER_FORMAT_POINT = 'media_viewer_format'
+const MEDIA_VIEWER_POINT = 'media_viewer'
+
+function mimeMatchesPattern(mime: string, pattern: string): boolean {
+ if (pattern.endsWith('/*')) {
+ return mime.startsWith(pattern.slice(0, -1))
+ }
+ return mime === pattern
+}
+
+function mimeMatchesViewer(mime: string, mimeTypes?: string[], mimePatterns?: string[]): boolean {
+ if (mimeTypes?.includes(mime)) return true
+ if (mimePatterns) {
+ for (const pattern of mimePatterns) {
+ if (mimeMatchesPattern(mime, pattern)) return true
+ }
+ }
+ return false
+}
+
+/** True if a leaf viewer is registered for this MIME type. */
+export function mediaViewerCanDisplay(mime: string | null | undefined): boolean {
+ if (!mime) return false
+ const viewers = useIntegrationStore().getItems(MEDIA_VIEWER_FORMAT_POINT)
+ return viewers.some(viewer =>
+ mimeMatchesViewer(
+ mime,
+ viewer.meta?.mimeTypes as string[] | undefined,
+ viewer.meta?.mimePatterns as string[] | undefined,
+ ),
+ )
+}
+
+export function mediaViewerComponent(type: 'dialog' | 'popover' = 'dialog') {
+ // The integration store prefixes ids with the module handle
+ // (e.g. 'media_viewer.dialog'), so match on the suffix.
+ const entry = useIntegrationStore().getItems(MEDIA_VIEWER_POINT).find(i => i.id.endsWith(`.${type}`))
+ return entry?.component
+ ? defineAsyncComponent(entry.component as () => Promise