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 0000000..2d09d2f
Binary files /dev/null and b/frontend/src/assets/dolby-atmos.webp differ
diff --git a/frontend/src/assets/dolby-vision-atmos.webp b/frontend/src/assets/dolby-vision-atmos.webp
new file mode 100644
index 0000000..70f74a1
Binary files /dev/null and b/frontend/src/assets/dolby-vision-atmos.webp differ
diff --git a/frontend/src/assets/dolby-vision.webp b/frontend/src/assets/dolby-vision.webp
new file mode 100644
index 0000000..8cfca88
Binary files /dev/null and b/frontend/src/assets/dolby-vision.webp differ
diff --git a/frontend/src/components/DolbyBadges.vue b/frontend/src/components/DolbyBadges.vue
new file mode 100644
index 0000000..3807998
--- /dev/null
+++ b/frontend/src/components/DolbyBadges.vue
@@ -0,0 +1,66 @@
+
+
+
![]()
+
+
+
+
+
+
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 @@
+
+
+ {{ label }}
+
+
+ {{ code }}
+
+
+
+
+
+
+
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'"
>
-
+): 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) }}
+
+
+ •
+
+
+
+
+
+