From a748adf87e07e606007fa33cb52a26a23e8e56dd Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 22 May 2026 18:26:29 +0000 Subject: [PATCH] feat(ui): redesign release card presentation --- frontend/package.json | 1 + frontend/src/assets/dolby-atmos.webp | Bin 0 -> 3934 bytes frontend/src/assets/dolby-vision-atmos.webp | Bin 0 -> 5670 bytes frontend/src/assets/dolby-vision.webp | Bin 0 -> 3886 bytes frontend/src/components/DolbyBadges.vue | 66 +++ frontend/src/components/LanguageFlags.vue | 109 +++++ frontend/src/components/MediaDetail.vue | 267 ++++++++++-- frontend/src/components/SeriesFullView.vue | 150 ++++++- frontend/src/types.ts | 3 + frontend/src/utils/languageFlags.ts | 446 ++++++++++++++++++++ mediahive/hivescan/indexer.py | 12 + mediahive/hivescan/showreel.py | 12 +- mediahive/models/data.py | 3 + 13 files changed, 1014 insertions(+), 55 deletions(-) create mode 100644 frontend/src/assets/dolby-atmos.webp create mode 100644 frontend/src/assets/dolby-vision-atmos.webp create mode 100644 frontend/src/assets/dolby-vision.webp create mode 100644 frontend/src/components/DolbyBadges.vue create mode 100644 frontend/src/components/LanguageFlags.vue create mode 100644 frontend/src/utils/languageFlags.ts diff --git a/frontend/package.json b/frontend/package.json index 3060f46..71a1795 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "country-flag-icons": "^1.6.17", "vue": "^3.4.0", "vue-router": "^4.6.4" }, diff --git a/frontend/src/assets/dolby-atmos.webp b/frontend/src/assets/dolby-atmos.webp new file mode 100644 index 0000000000000000000000000000000000000000..2d09d2f8e958f299980c5657a7e2247e2e847d02 GIT binary patch literal 3934 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/dolby-vision-atmos.webp b/frontend/src/assets/dolby-vision-atmos.webp new file mode 100644 index 0000000000000000000000000000000000000000..70f74a1b91ed8dcec5422c69a128e4ec10d2991e GIT binary patch literal 5670 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 JxtpUQ0|32=PVWE! literal 0 HcmV?d00001 diff --git a/frontend/src/assets/dolby-vision.webp b/frontend/src/assets/dolby-vision.webp new file mode 100644 index 0000000000000000000000000000000000000000..8cfca885b7f088b27534be463d12179af91748db GIT binary patch literal 3886 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 +
+ +
+ + + + + diff --git a/frontend/src/components/LanguageFlags.vue b/frontend/src/components/LanguageFlags.vue new file mode 100644 index 0000000..1c5351b --- /dev/null +++ b/frontend/src/components/LanguageFlags.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/components/MediaDetail.vue b/frontend/src/components/MediaDetail.vue index cfd5c6f..e37ef0d 100644 --- a/frontend/src/components/MediaDetail.vue +++ b/frontend/src/components/MediaDetail.vue @@ -91,29 +91,43 @@ v-for="(version, index) in movieVersions" :key="index" class="version-row" - :class="{ 'version-best': index === 0 }" + :class="{ + 'version-best': index === 0, + 'version-selectable': !!version.playable_file, + 'version-disabled': !version.playable_file, + }" + tabindex="0" + v-bind="navAttrs(2, index)" + @click="handleVersionActivate(version, $event)" + @keydown.enter.prevent="handleVersionActivate(version, $event)" + @keydown.space.prevent="handleVersionActivate(version, $event)" + :title="version.playable_file ? 'Click to play/continue. Alt+Click to open folder.' : 'No playable file'" > -
+
{{ version.resolution }} - {{ version.quality }} - {{ version.codec }} - {{ version.audio }} + {{ getDisplayQualityBadge(version) }} + {{ getDisplayCodecBadge(version) }} + HDR + {{ getDisplayAudioBadge(version) }} 💿
+
+ + + +
-
- - +
+
@@ -178,7 +192,10 @@ import { getCoverUrl, getVideoPreviewUrl, getVideoSourceAttributes, isSafariBrow import castPlaceholderFemaleUrl from '../assets/cast-placeholder-female.svg'; import castPlaceholderMaleUrl from '../assets/cast-placeholder-male.svg'; import SeriesFullView from './SeriesFullView.vue'; +import LanguageFlags from './LanguageFlags.vue'; +import DolbyBadges from './DolbyBadges.vue'; import { navAttrs } from '../composables/useKeyboardNavigation'; +import { buildLanguageFlags } from '../utils/languageFlags'; const props = defineProps<{ item: MediaItem; @@ -385,6 +402,74 @@ const synopsisPosterUrl = computed(() => { return getCoverUrl(props.item.cover_path); }); +const dolbyTagPattern = /\b(dolby|atmos|vision|dovi|dv)\b/i; +const dolbyVisionPattern = /\b(dolby\s*vision|dovi|\bdv\b)\b/i; +const dolbyAtmosPattern = /\b(dolby\s*atmos|atmos)\b/i; +const hdrPattern = /\bhdr\b|smpte\s*2084|bt\s*2020|hlg/i; +const blurayTagPattern = /\bblu[\s.-]*ray\b/i; + +function hasDolbyTag(value: string | null | undefined): boolean { + return Boolean(value && dolbyTagPattern.test(value)); +} + +function hasAnyTag( + pattern: RegExp, + ...values: Array +): boolean { + return values.some((value) => Boolean(value && pattern.test(value))); +} + +function getHasDolbyVision(version: Torrent): boolean { + return ( + version.has_dolby_vision === true + || hasAnyTag(dolbyVisionPattern, version.quality, version.codec, version.audio, version.title) + ); +} + +function getHasDolbyAtmos(version: Torrent): boolean { + return ( + version.has_dolby_atmos === true + || hasAnyTag(dolbyAtmosPattern, version.quality, version.codec, version.audio, version.title) + ); +} + +function getHasHdr(version: Torrent): boolean { + return ( + version.is_hdr === true + || hasAnyTag(hdrPattern, version.quality, version.codec, version.audio, version.title) + ); +} + +function getDisplayQualityBadge(version: Torrent): string | null { + if (!version.quality || hasDolbyTag(version.quality)) return null; + if (blurayTagPattern.test(version.quality) && !isVersionDisc(version)) return null; + return version.quality; +} + +function getDisplayCodecBadge(version: Torrent): string | null { + if (!version.codec || hasDolbyTag(version.codec)) return null; + return version.codec; +} + +function getDisplayAudioBadge(version: Torrent): string | null { + if (!version.audio || hasDolbyTag(version.audio)) return null; + return version.audio; +} + +function hasLanguageDisplay(codes: string[] | null | undefined): boolean { + const mapped = buildLanguageFlags(codes); + return mapped.flags.length > 0 || mapped.unmappedCodes.length > 0; +} + +function hasHdrTag(value: string | null | undefined): boolean { + return Boolean(value && hdrPattern.test(value)); +} + +function getShowHdrBadge(version: Torrent): boolean { + if (!getHasHdr(version)) return false; + return !hasHdrTag(version.quality) && !hasHdrTag(version.codec) && !hasHdrTag(version.audio); +} + // Check if a specific version is a disc format (Blu-ray disc has index.bdmv) function isVersionDisc(version: Torrent): boolean { if (!version.playable_file) return false; @@ -485,8 +570,13 @@ function handlePlay(filePath: string | null) { } } -function getPlayLabel(filePath: string | null): string { - return props.hasResumePosition(filePath) ? 'Continue' : 'Play'; +function handleVersionActivate(version: Torrent, event: MouseEvent | KeyboardEvent) { + if (!version.playable_file) return; + if (event.altKey) { + handleOpenFolder(version.playable_file); + return; + } + handlePlay(version.playable_file); } function handleOpenFolder(folderPath: string) { @@ -938,6 +1028,75 @@ function handleOpenFolder(folderPath: string) { gap: 6px; } +.version-language-flags { + display: inline-flex; + align-items: center; + gap: 2px; + width: 100%; + white-space: nowrap; + overflow: hidden; + min-width: 0; +} + +.version-language-flags > .language-flags-audio { + flex: 0 0 auto; +} + +.version-language-flags > .language-flags-subs { + flex: 1 1 auto; + min-width: 0; + -webkit-mask-image: linear-gradient(to right, black calc(100% - 18px), transparent); + mask-image: linear-gradient(to right, black calc(100% - 18px), transparent); +} + +.version-language-flags > .language-flags-subs :deep(.language-flags) { + display: inline-flex; + max-width: 100%; + overflow: hidden; +} + +.version-language-flags > .language-flags-subs :deep(.language-flag-list) { + width: max-content; + max-width: none; + overflow: hidden; +} + +.language-separator { + color: rgba(255, 255, 255, 0.8); + font-size: 0.98rem; + font-weight: 700; + line-height: 1; + margin: 0; +} + +.version-meta-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + grid-template-rows: auto auto; + column-gap: 8px; + row-gap: 6px; + align-items: stretch; +} + +.version-meta-grid .version-badges { + grid-column: 1; + grid-row: 1; +} + +.version-meta-grid .version-language-flags { + grid-column: 1; + grid-row: 2; +} + +.version-dolby { + grid-column: 2; + grid-row: 1 / span 2; + align-self: stretch; + justify-self: end; + display: flex; + min-width: 0; +} + .version-badge { font-size: 0.75rem; padding: 2px 8px; @@ -983,7 +1142,7 @@ function handleOpenFolder(folderPath: string) { display: flex; gap: 8px; flex-shrink: 0; - margin-left: 16px; + margin-left: 0; } /* Season header styling */ @@ -1402,12 +1561,13 @@ function handleOpenFolder(folderPath: string) { } .version-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + align-items: stretch; + column-gap: 8px; + row-gap: 6px; padding: 12px 16px; - background: rgba(0, 0, 0, 0.4); + background: rgba(10, 14, 22, 0.2); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 8px; @@ -1416,28 +1576,53 @@ function handleOpenFolder(folderPath: string) { } .version-row:hover { - background: rgba(0, 0, 0, 0.5); + background: rgba(10, 14, 22, 0.28); border-color: rgba(255, 255, 255, 0.2); } .version-row.version-best { - border-color: rgba(70, 211, 105, 0.5); - background: rgba(70, 211, 105, 0.15); + border-color: rgba(255, 255, 255, 0.1); + background: rgba(10, 14, 22, 0.2); } .version-row.version-best:hover { - background: rgba(70, 211, 105, 0.25); + background: rgba(10, 14, 22, 0.28); + border-color: rgba(255, 255, 255, 0.2); } -.version-info { - flex: 1; +.version-row.version-selectable { + cursor: pointer; +} + +.version-row.version-selectable:focus-visible { + outline: 2px solid rgba(255, 255, 255, 0.85); + outline-offset: 2px; +} + +.version-row.version-disabled { + cursor: not-allowed; + opacity: 0.75; +} + +.version-main { + grid-column: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: 6px; min-width: 0; } -.version-actions { +.version-dolby-cell { + grid-column: 2; display: flex; - gap: 8px; - flex-shrink: 0; + align-items: stretch; + justify-content: flex-end; + min-width: 0; +} + +.version-dolby { + align-self: stretch; } .btn-small { @@ -1489,17 +1674,9 @@ function handleOpenFolder(folderPath: string) { color: #fffbeb; } -.version-sub { - font-size: 0.65rem; - color: var(--text-muted, rgba(255, 255, 255, 0.5)); +.v-badge.hdr { + background: #166534; + color: #dcfce7; } -/* Collage images for header */ -.collage-images { - position: absolute; - inset: 0; - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 2px; -} diff --git a/frontend/src/components/SeriesFullView.vue b/frontend/src/components/SeriesFullView.vue index 7214f44..3e5b8b0 100644 --- a/frontend/src/components/SeriesFullView.vue +++ b/frontend/src/components/SeriesFullView.vue @@ -138,7 +138,25 @@ :key="index" class="context-menu-version" > -
{{ getVersionLabel(torrent) }}
+
+
{{ getVersionLabel(torrent) }}
+
+ + + +
+
+
+ +