Merge remote-tracking branch 'remotes/upstream/master'

This commit is contained in:
Toni Barth 2025-12-04 07:55:56 +01:00
commit 4d669fd670
67 changed files with 2415 additions and 487 deletions

View file

@ -93,10 +93,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
editItem(libraryItem, tab = 'details') {
var itemIds = this.shelf.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])

View file

@ -232,11 +232,11 @@ export default {
clearFilter() {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
},
editEntity(entity) {
editEntity(entity, tab = 'details') {
if (this.entityName === 'items' || this.entityName === 'series-books') {
const bookIds = this.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', entity)
this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' })
} else if (this.entityName === 'collections') {
this.$store.commit('globals/setEditCollection', entity)
} else if (this.entityName === 'playlists') {

View file

@ -62,7 +62,24 @@
</widgets-alert>
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center z-20">
<ui-loading-indicator :text="nonInteractionLabel" />
<ui-loading-indicator>
<div class="mb-4">
<span class="text-lg font-medium text-white">
{{ nonInteractionLabel }}
</span>
</div>
<div v-if="isUploading" class="w-64 mx-auto mb-2">
<div class="flex items-center justify-center mb-2">
<span class="text-sm font-medium text-white/60 text-center w-full">
{{ uploadProgressText }}
</span>
</div>
<div class="w-full bg-primary/20 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full transition-all duration-300 ease-out" :style="{ width: uploadProgressPercent + '%' }"></div>
</div>
</div>
</ui-loading-indicator>
</div>
</div>
</template>
@ -91,7 +108,11 @@ export default {
isUploading: false,
uploadFailed: false,
uploadSuccess: false,
isFetchingMetadata: false
isFetchingMetadata: false,
uploadProgress: {
loaded: 0,
total: 0
}
}
},
computed: {
@ -116,6 +137,15 @@ export default {
} else if (this.isFetchingMetadata) {
return this.$strings.LabelFetchingMetadata
}
},
uploadProgressPercent() {
if (this.uploadProgress.total === 0) return 0
return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100))
},
uploadProgressText() {
const loaded = this.$bytesPretty(this.uploadProgress.loaded)
const total = this.$bytesPretty(this.uploadProgress.total)
return `${this.uploadProgressPercent}% (${loaded} / ${total})`
}
},
methods: {
@ -123,6 +153,21 @@ export default {
this.isUploading = status === 'uploading'
this.uploadFailed = status === 'failed'
this.uploadSuccess = status === 'success'
if (status !== 'uploading') {
this.uploadProgress = {
loaded: 0,
total: 0
}
}
},
setUploadProgress(progress) {
if (this.isUploading && progress) {
this.uploadProgress = {
loaded: progress.loaded || 0,
total: progress.total || 0
}
}
},
titleUpdated() {
this.error = ''

View file

@ -788,11 +788,11 @@ export default {
},
showEditModalFiles() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' })
this.$emit('edit', this.libraryItem, 'files')
},
showEditModalMatch() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
this.$emit('edit', this.libraryItem, 'match')
},
sendToDevice(deviceName) {
// More menu func

View file

@ -88,7 +88,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
@ -96,6 +96,9 @@ export default {
},
methods: {
init() {
// Fetch providers when modal is shown
this.$store.dispatch('scanners/fetchProviders')
// If we don't have a set provider (first open of dialog) or we've switched library, set
// the selected provider to the current library default provider
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
@ -127,8 +130,7 @@ export default {
this.show = false
})
}
},
mounted() {}
}
}
</script>

View file

@ -133,8 +133,8 @@ export default {
}
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
if (this.isPodcast) return this.$store.state.scanners.podcastCoverProviders
return this.$store.state.scanners.bookCoverProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@ -438,6 +438,8 @@ export default {
mounted() {
// Setup socket listeners when component is mounted
this.addSocketListeners()
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
},
beforeDestroy() {
// Cancel any ongoing search when component is destroyed

View file

@ -2,7 +2,7 @@
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
<form @submit.prevent="submitSearch">
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
<div class="w-36 px-1">
<div v-if="providersLoaded" class="w-36 px-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div>
<div class="grow md:w-72 px-1">
@ -253,6 +253,7 @@ export default {
hasSearched: false,
selectedMatch: null,
selectedMatchOrig: null,
waitingForProviders: false,
selectedMatchUsage: {
title: true,
subtitle: true,
@ -285,9 +286,19 @@ export default {
handler(newVal) {
if (newVal) this.init()
}
},
providersLoaded(isLoaded) {
// Complete initialization once providers are loaded
if (isLoaded && this.waitingForProviders) {
this.waitingForProviders = false
this.initProviderAndSearch()
}
}
},
computed: {
providersLoaded() {
return this.$store.getters['scanners/areProvidersLoaded']
},
isProcessing: {
get() {
return this.processing
@ -319,7 +330,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@ -478,6 +489,24 @@ export default {
this.checkboxToggled()
},
initProviderAndSearch() {
// Set provider based on media type
if (this.isPodcast) {
this.provider = 'itunes'
} else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
this.searchTitle = this.libraryItem.media.metadata.asin
this.searchAuthor = ''
}
if (this.searchTitle) {
this.submitSearch()
}
},
init() {
this.clearSelectedMatch()
this.initSelectedMatchUsage()
@ -495,19 +524,13 @@ export default {
}
this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
this.searchTitle = this.libraryItem.media.metadata.asin
this.searchAuthor = ''
}
if (this.searchTitle) {
this.submitSearch()
// Wait for providers to be loaded before setting provider and searching
if (this.providersLoaded || this.isPodcast) {
this.waitingForProviders = false
this.initProviderAndSearch()
} else {
this.waitingForProviders = true
}
},
selectMatch(match) {
@ -637,6 +660,10 @@ export default {
this.selectedMatch = null
this.selectedMatchOrig = null
}
},
mounted() {
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
}
}
</script>

View file

@ -74,7 +74,7 @@ export default {
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
}
},
methods: {
@ -156,6 +156,8 @@ export default {
},
mounted() {
this.init()
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
}
}
</script>

View file

@ -104,7 +104,6 @@ export default {
},
data() {
return {
provider: null,
useSquareBookCovers: false,
enableWatcher: false,
skipMatchingMediaWithAsin: false,
@ -134,10 +133,6 @@ export default {
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
},
maskAsFinishedWhenItems() {
return [
{

View file

@ -186,10 +186,16 @@ export default {
daysInARow() {
var count = 0
while (true) {
var _date = this.$addDaysToToday(count * -1)
var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
const _date = this.$addDaysToToday(count * -1 - 1)
const datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) {
// don't require listening today to count towards days in a row, but do count it if already listened today
const today = this.$formatJsDate(new Date(), 'yyyy-MM-dd')
if (this.listeningStatsDays[today]) {
count++
}
return count
}
count++

View file

@ -1,5 +1,5 @@
<template>
<div class="w-40">
<div :class="hasSlotContent ? 'w-auto' : 'w-40'">
<div class="bg-bg border border-gray-500 py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="loader-dots block relative w-20 h-5 mt-2">
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
@ -7,7 +7,9 @@
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
</div>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
<slot>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
</slot>
</div>
</div>
</template>
@ -23,6 +25,9 @@ export default {
computed: {
message() {
return this.text || this.$strings.MessagePleaseWait
},
hasSlotContent() {
return this.$slots.default && this.$slots.default.length > 0
}
}
}

View file

@ -132,10 +132,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
editItem(libraryItem, tab = 'details') {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
selectItem(payload) {
this.$emit('selectEntity', payload)