feat(ui): redesign release card presentation
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div v-if="showDolbyLogo" class="dolby-badges" :class="{ compact }">
|
||||
<img
|
||||
:src="dolbyLogoSrc"
|
||||
:alt="dolbyLogoAlt"
|
||||
class="dolby-logo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import dolbyAtmosUrl from '../assets/dolby-atmos.webp';
|
||||
import dolbyVisionUrl from '../assets/dolby-vision.webp';
|
||||
import dolbyVisionAtmosUrl from '../assets/dolby-vision-atmos.webp';
|
||||
|
||||
const props = defineProps<{
|
||||
hasDolbyVision?: boolean | null;
|
||||
hasDolbyAtmos?: boolean | null;
|
||||
isHdr?: boolean | null;
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const hasDolbyVision = computed(() => props.hasDolbyVision === true);
|
||||
const hasDolbyAtmos = computed(() => props.hasDolbyAtmos === true);
|
||||
const compact = computed(() => props.compact === true);
|
||||
|
||||
const showDolbyLogo = computed(() => hasDolbyVision.value || hasDolbyAtmos.value);
|
||||
|
||||
const dolbyLogoSrc = computed(() => {
|
||||
if (hasDolbyVision.value && hasDolbyAtmos.value) return dolbyVisionAtmosUrl;
|
||||
if (hasDolbyVision.value) return dolbyVisionUrl;
|
||||
return dolbyAtmosUrl;
|
||||
});
|
||||
|
||||
const dolbyLogoAlt = computed(() => {
|
||||
if (hasDolbyVision.value && hasDolbyAtmos.value) return 'Dolby Vision + Dolby Atmos';
|
||||
if (hasDolbyVision.value) return 'Dolby Vision';
|
||||
return 'Dolby Atmos';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dolby-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dolby-logo {
|
||||
height: 100%;
|
||||
max-height: 44px;
|
||||
min-height: 30px;
|
||||
width: auto;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
filter: invert(1) brightness(1.1) contrast(1.05);
|
||||
}
|
||||
|
||||
.dolby-badges.compact .dolby-logo {
|
||||
max-height: 34px;
|
||||
min-height: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div v-if="hasContent" class="language-flags" :class="{ compact }">
|
||||
<span v-if="label" class="language-flags-label">{{ label }}</span>
|
||||
<span class="language-flag-list">
|
||||
<span
|
||||
v-for="entry in flagEntries"
|
||||
:key="entry.countryCode"
|
||||
class="language-flag"
|
||||
:title="`${entry.countryCode}: ${entry.sourceCodes.join(', ')}`"
|
||||
v-html="entry.svg"
|
||||
></span>
|
||||
<span
|
||||
v-for="code in unmappedCodes"
|
||||
:key="`raw-${code}`"
|
||||
class="language-code-fallback"
|
||||
:title="code"
|
||||
>{{ code }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { buildLanguageFlags } from '../utils/languageFlags';
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string;
|
||||
codes: string[] | null | undefined;
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const mapped = computed(() => buildLanguageFlags(props.codes));
|
||||
const flagEntries = computed(() => mapped.value.flags);
|
||||
const unmappedCodes = computed(() => mapped.value.unmappedCodes);
|
||||
const hasContent = computed(() => flagEntries.value.length > 0 || unmappedCodes.value.length > 0);
|
||||
const compact = computed(() => props.compact === true);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.language-flags {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.language-flags-label {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.language-flag-list {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35) inset;
|
||||
}
|
||||
|
||||
.language-flag :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-code-fallback {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.58rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 3px;
|
||||
padding: 0.08rem 0.25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.language-flags.compact .language-flags-label {
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.language-flags.compact .language-flag {
|
||||
width: 16px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.language-flags.compact .language-code-fallback {
|
||||
font-size: 0.52rem;
|
||||
}
|
||||
</style>
|
||||
@@ -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'"
|
||||
>
|
||||
<div class="version-info">
|
||||
<div class="version-main">
|
||||
<div class="version-badges">
|
||||
<span v-if="version.resolution" class="v-badge res">{{ version.resolution }}</span>
|
||||
<span v-if="version.quality" class="v-badge qual">{{ version.quality }}</span>
|
||||
<span v-if="version.codec" class="v-badge codec">{{ version.codec }}</span>
|
||||
<span v-if="version.audio" class="v-badge audio">{{ version.audio }}</span>
|
||||
<span v-if="getDisplayQualityBadge(version)" class="v-badge qual">{{ getDisplayQualityBadge(version) }}</span>
|
||||
<span v-if="getDisplayCodecBadge(version)" class="v-badge codec">{{ getDisplayCodecBadge(version) }}</span>
|
||||
<span v-if="getShowHdrBadge(version)" class="v-badge hdr">HDR</span>
|
||||
<span v-if="getDisplayAudioBadge(version)" class="v-badge audio">{{ getDisplayAudioBadge(version) }}</span>
|
||||
<span v-if="isVersionDisc(version)" class="v-disc">💿</span>
|
||||
</div>
|
||||
<div class="version-language-flags">
|
||||
<LanguageFlags class="language-flags-audio" :codes="version.audio_languages" />
|
||||
<span
|
||||
v-if="hasLanguageDisplay(version.audio_languages) && hasLanguageDisplay(version.subtitle_languages)"
|
||||
class="language-separator"
|
||||
>•</span>
|
||||
<LanguageFlags class="language-flags-subs" :codes="version.subtitle_languages" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<button
|
||||
class="btn btn-small btn-primary"
|
||||
v-bind="navAttrs(2, index * 2)"
|
||||
@click="handlePlay(version.playable_file)"
|
||||
:disabled="!version.playable_file"
|
||||
>▶ {{ getPlayLabel(version.playable_file) }}</button>
|
||||
<button
|
||||
class="btn btn-small btn-secondary"
|
||||
v-bind="navAttrs(2, index * 2 + 1)"
|
||||
@click="handleOpenFolder(version.playable_file || '')"
|
||||
>📁</button>
|
||||
<div class="version-dolby-cell">
|
||||
<DolbyBadges
|
||||
class="version-dolby"
|
||||
:has-dolby-vision="getHasDolbyVision(version)"
|
||||
:has-dolby-atmos="getHasDolbyAtmos(version)"
|
||||
:is-hdr="getHasHdr(version)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string | null | undefined>
|
||||
): 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -138,7 +138,25 @@
|
||||
:key="index"
|
||||
class="context-menu-version"
|
||||
>
|
||||
<div class="version-label">{{ getVersionLabel(torrent) }}</div>
|
||||
<div class="version-main">
|
||||
<div class="version-label">{{ getVersionLabel(torrent) }}</div>
|
||||
<div class="version-language-flags">
|
||||
<LanguageFlags class="language-flags-audio" :codes="torrent.audio_languages" compact />
|
||||
<span
|
||||
v-if="hasLanguageDisplay(torrent.audio_languages) && hasLanguageDisplay(torrent.subtitle_languages)"
|
||||
class="language-separator"
|
||||
>•</span>
|
||||
<LanguageFlags class="language-flags-subs" :codes="torrent.subtitle_languages" compact />
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-dolby-cell">
|
||||
<DolbyBadges
|
||||
class="version-dolby"
|
||||
:has-dolby-vision="getHasDolbyVision(torrent)"
|
||||
:has-dolby-atmos="getHasDolbyAtmos(torrent)"
|
||||
:is-hdr="getHasHdr(torrent)"
|
||||
/>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<button
|
||||
class="ctx-btn ctx-btn-play"
|
||||
@@ -167,6 +185,9 @@ import { computed, ref, nextTick, watch } from 'vue';
|
||||
import type { Series, Season, Episode, Torrent } from '../types';
|
||||
import { getCoverUrl, getVideoPreviewUrl, getVideoSourceAttributes, isSafariBrowser } from '../api';
|
||||
import { navAttrs } from '../composables/useKeyboardNavigation';
|
||||
import LanguageFlags from './LanguageFlags.vue';
|
||||
import DolbyBadges from './DolbyBadges.vue';
|
||||
import { buildLanguageFlags } from '../utils/languageFlags';
|
||||
|
||||
const props = defineProps<{
|
||||
series: Series;
|
||||
@@ -320,13 +341,59 @@ function handleOpenFolder(folderPath: string) {
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
function hasDolbyTag(value: string | null | undefined): boolean {
|
||||
return Boolean(value && dolbyTagPattern.test(value));
|
||||
}
|
||||
|
||||
function hasAnyTag(
|
||||
pattern: RegExp,
|
||||
...values: Array<string | null | undefined>
|
||||
): boolean {
|
||||
return values.some((value) => Boolean(value && pattern.test(value)));
|
||||
}
|
||||
|
||||
function getHasDolbyVision(torrent: Torrent): boolean {
|
||||
return (
|
||||
torrent.has_dolby_vision === true
|
||||
|| hasAnyTag(dolbyVisionPattern, torrent.quality, torrent.codec, torrent.audio, torrent.title)
|
||||
);
|
||||
}
|
||||
|
||||
function getHasDolbyAtmos(torrent: Torrent): boolean {
|
||||
return (
|
||||
torrent.has_dolby_atmos === true
|
||||
|| hasAnyTag(dolbyAtmosPattern, torrent.quality, torrent.codec, torrent.audio, torrent.title)
|
||||
);
|
||||
}
|
||||
|
||||
function getHasHdr(torrent: Torrent): boolean {
|
||||
return (
|
||||
torrent.is_hdr === true
|
||||
|| hasAnyTag(hdrPattern, torrent.quality, torrent.codec, torrent.audio, torrent.title)
|
||||
);
|
||||
}
|
||||
|
||||
function hasLanguageDisplay(codes: string[] | null | undefined): boolean {
|
||||
const mapped = buildLanguageFlags(codes);
|
||||
return mapped.flags.length > 0 || mapped.unmappedCodes.length > 0;
|
||||
}
|
||||
|
||||
// Get version display label
|
||||
function getVersionLabel(torrent: Torrent): string {
|
||||
const parts: string[] = [];
|
||||
if (torrent.resolution) parts.push(torrent.resolution);
|
||||
if (torrent.quality) parts.push(torrent.quality);
|
||||
if (torrent.codec) parts.push(torrent.codec);
|
||||
if (torrent.audio) parts.push(torrent.audio);
|
||||
if (torrent.quality && !hasDolbyTag(torrent.quality)) parts.push(torrent.quality);
|
||||
if (torrent.codec && !hasDolbyTag(torrent.codec)) parts.push(torrent.codec);
|
||||
if (torrent.audio && !hasDolbyTag(torrent.audio)) parts.push(torrent.audio);
|
||||
const hasExistingHdr = [torrent.quality, torrent.codec, torrent.audio].some(
|
||||
(value) => Boolean(value && hdrPattern.test(value))
|
||||
);
|
||||
if (getHasHdr(torrent) && !hasExistingHdr) parts.push('HDR');
|
||||
return parts.length > 0 ? parts.join(' • ') : 'Unknown';
|
||||
}
|
||||
|
||||
@@ -931,11 +998,12 @@ function handlePlay(episode: Episode) {
|
||||
}
|
||||
|
||||
.context-menu-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) max-content max-content;
|
||||
align-items: stretch;
|
||||
padding: 10px 16px;
|
||||
gap: 12px;
|
||||
column-gap: 8px;
|
||||
row-gap: 6px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
@@ -947,15 +1015,79 @@ function handlePlay(episode: Episode) {
|
||||
.version-label {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-main {
|
||||
grid-column: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.version-dolby-cell {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.version-dolby {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.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% - 14px), transparent);
|
||||
mask-image: linear-gradient(to right, black calc(100% - 14px), 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.9rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user