feat: implement server-side cover dimension detection and update badge thresholds

This commit is contained in:
Tiberiu Ichim 2026-02-17 19:30:48 +02:00
parent 5bf60d5ae3
commit d0c09d04f1
16 changed files with 353 additions and 10 deletions

View file

@ -122,6 +122,11 @@
<span class="material-symbols text-black" :style="{ fontSize: 0.875 + 'em' }">folder_open</span>
</button>
</ui-tooltip>
<!-- Cover Size Badge -->
<div v-if="coverBadge" class="absolute rounded-sm text-white font-bold pointer-events-none z-20" :class="coverBadge.color" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em`, fontSize: 0.6 + 'em', lineHeight: 0.8 + 'em' }">
{{ coverBadge.text }}
</div>
</div>
</div>
@ -176,7 +181,9 @@ export default {
isSelectionMode: false,
displayTitleTruncated: false,
displaySubtitleTruncated: false,
showCoverBg: false
showCoverBg: false,
naturalWidth: 0,
naturalHeight: 0
}
},
watch: {
@ -250,6 +257,18 @@ export default {
if (this.collapsedSeries?.name) return this.collapsedSeries.name
return this.series?.name || null
},
coverBadge() {
const width = this.media?.coverWidth || this.coverWidth
const height = this.media?.coverHeight || this.coverHeight
if (!width || !height) return null
if (width >= 1200 || height >= 1200) {
return { text: 'BIG', color: 'bg-success' }
}
if (width >= 450 || height >= 450) {
return { text: 'MED', color: 'bg-info' }
}
return { text: 'SML', color: 'bg-warning' }
},
seriesSequence() {
return this.series?.sequence || null
},
@ -1148,6 +1167,8 @@ export default {
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
this.naturalWidth = naturalWidth
this.naturalHeight = naturalHeight
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)

View file

@ -22,6 +22,11 @@
</div>
</div>
<!-- Cover Size Badge -->
<div v-if="coverBadge" class="absolute bottom-1 right-1 px-1 rounded-sm text-white font-bold pointer-events-none z-20" :class="coverBadge.color" :style="{ fontSize: (0.6 * sizeMultiplier) + 'rem', lineHeight: (0.8 * sizeMultiplier) + 'rem' }">
{{ coverBadge.text }}
</div>
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div>
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
@ -52,7 +57,9 @@ export default {
loading: true,
imageFailed: false,
showCoverBg: false,
imageReady: false
imageReady: false,
naturalWidth: 0,
naturalHeight: 0
}
},
watch: {
@ -133,6 +140,18 @@ export default {
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
},
coverBadge() {
const width = this.media?.coverWidth || this.naturalWidth
const height = this.media?.coverHeight || this.naturalHeight
if (!width || !height) return null
if (width >= 1200 || height >= 1200) {
return { text: 'BIG', color: 'bg-success' }
}
if (width >= 450 || height >= 450) {
return { text: 'MED', color: 'bg-info' }
}
return { text: 'SML', color: 'bg-warning' }
}
},
methods: {
@ -154,6 +173,8 @@ export default {
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
this.naturalWidth = naturalWidth
this.naturalHeight = naturalHeight
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)

View file

@ -49,6 +49,18 @@
</div>
</div>
</div>
<div class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center">
<div>
<p class="text-lg">{{ $strings.LabelUpdateCoverDimensions }}</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelUpdateCoverDimensionsHelp }}</p>
</div>
<div class="grow" />
<div>
<ui-btn @click.stop="updateCoverDimensionsClick">{{ $strings.ButtonUpdate }}</ui-btn>
</div>
</div>
</div>
</div>
</template>
@ -198,6 +210,34 @@ export default {
.finally(() => {
this.$emit('update:processing', false)
})
},
updateCoverDimensionsClick() {
const payload = {
message: this.$strings.MessageConfirmUpdateCoverDimensions,
persistent: true,
callback: (confirmed) => {
if (confirmed) {
this.updateCoverDimensions()
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
updateCoverDimensions() {
this.$emit('update:processing', true)
this.$axios
.$post(`/api/libraries/${this.libraryId}/update-cover-dimensions`)
.then((data) => {
this.$toast.success(this.$getString('ToastUpdateCoverDimensionsSuccess', [data.updated]))
})
.catch((error) => {
console.error('Failed to update cover dimensions', error)
this.$toast.error(this.$strings.ToastUpdateCoverDimensionsFailed)
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {}

View file

@ -289,6 +289,8 @@
"LabelCleanupAuthorsHelp": "Remove authors that have no books in this library.",
"LabelUpdateConsolidationStatus": "Update Consolidation Status",
"LabelUpdateConsolidationStatusHelp": "Checks all items in this library and updates their consolidation status. This is useful if you have manually moved folders on disk.",
"LabelUpdateCoverDimensions": "Update Cover Dimensions",
"LabelUpdateCoverDimensionsHelp": "Detect and update cover width and height for all items in this library.",
"LabelClickForMoreInfo": "Click for more info",
"LabelClickToUseCurrentValue": "Click to use current value",
"LabelClosePlayer": "Close player",
@ -810,6 +812,7 @@
"MessageConfirmRemoveItemsWithIssues": "Are you sure you want to remove all items with issues?",
"MessageConfirmCleanupAuthors": "Are you sure you want to remove all authors with no books in this library?",
"MessageConfirmUpdateConsolidationStatus": "Are you sure you want to update the consolidation status for all items in this library? This will re-calculate the 'Not Consolidated' badge for every book.",
"MessageConfirmUpdateCoverDimensions": "Are you sure you want to update cover dimensions for all items in this library? This will use ffprobe to detect dimensions for each cover.",
"MessageConfirmRemoveEpisodeNote": "Note: This does not delete the audio file unless toggling \"Hard delete file\"",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
@ -1194,6 +1197,8 @@
"ToastUserRootRequireName": "Must enter a root username",
"ToastUpdateConsolidationStatusSuccess": "Successfully updated consolidation status for {0} items.",
"ToastUpdateConsolidationStatusFailed": "Failed to update consolidation status.",
"ToastUpdateCoverDimensionsSuccess": "Successfully updated cover dimensions for {0} items.",
"ToastUpdateCoverDimensionsFailed": "Failed to update cover dimensions.",
"TooltipAddChapters": "Add chapter(s)",
"TooltipAddOneSecond": "Add 1 second",
"TooltipAdjustChapterStart": "Click to adjust start time",