mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c58a6b9047 | |||
| b787fb18f3 | |||
| 17cce9c914 | |||
| 90299e348c | |||
| fe25a1bc54 | |||
| edbe1851b5 | |||
| ad6c5a4f00 | |||
| 4971787482 | |||
| 56d2ec9c22 | |||
| 106ddc9541 | |||
| 4d93e39fa9 | |||
| 54b41b15c2 | |||
| 54ca42a903 | |||
| d7cc8a052a | |||
| 5165f11460 | |||
| b47ce4fb24 | |||
| 9b1f7f566f | |||
| 10295b000a | |||
| c06d734d5e | |||
| 49a69193d8 | |||
| 7852804a9c | |||
| 415dda37a4 | |||
| 179d339afd | |||
| 858c1a7353 | |||
| 0b42b81558 | |||
| f9678dec2f | |||
| 82642b295c | |||
| ba3d84a924 | |||
| 96e2f934a3 | |||
| a68ade2b3d | |||
| 4fcdeda447 | |||
| dc03835742 | |||
| 50430e6b27 | |||
| d130dd6d5e | |||
| 793cc989de | |||
| 27d8c4d67c | |||
| 48f493a9f5 | |||
| 04992ee3fb | |||
| 4d8e2a1279 | |||
| 2af7b6b6f1 | |||
| e59351566d | |||
| 05d10b73c3 | |||
| 41e192c6a5 | |||
| ea42ab7624 | |||
| 2d9035d90b | |||
| 0ae853c119 | |||
| 3c0fdff7b4 | |||
| eede2bbd46 | |||
| 5c31687a0f | |||
| 6b654d3c2d | |||
| 91cbe45839 | |||
| 7883d4a97f | |||
| 9f4547cff8 | |||
| a98106593d | |||
| c625b3f08c | |||
| 9e7f09c21b | |||
| 616caecdf1 | |||
| cee19c5128 | |||
| 67db41a525 | |||
| 3ea3e55d17 | |||
| 4959a28485 | |||
| 6d2482a98e | |||
| 4b23b842bb | |||
| 07bebc8808 | |||
| 027d7f7a5b | |||
| 6baa0fa047 | |||
| 8425fac543 | |||
| 7b2ac7b9e9 | |||
| bf071be247 | |||
| 6c05a0af8a | |||
| 0e292c64c4 | |||
| 725f8eecdb | |||
| 521a673094 | |||
| d917f0e37d | |||
| 7ed5b1744f | |||
| c9ab2a242d | |||
| 13532cba14 | |||
| 3fb2bd3362 | |||
| e80c3a1c5a | |||
| e04d26307e | |||
| b8f74e1c98 | |||
| 0851050392 | |||
| b84882d9d1 | |||
| cd37a7618e | |||
| 64a7cfac3b | |||
| 1ee7ba54f8 | |||
| 6bb18f8800 | |||
| b26b854963 | |||
| 7d58361ced | |||
| a3723f3d06 | |||
| 78d1cd0cfb | |||
| d41366a417 | |||
| a2347150a2 | |||
| d33f23dede | |||
| cfca2be1b2 | |||
| 73f07c1392 | |||
| 4541e9ddc3 | |||
| 972271a1a9 | |||
| e97d92a8ac | |||
| 9a73e352d1 | |||
| 08f09f81fa | |||
| c72609013c | |||
| 29a6434fdc | |||
| eb2ea9950a | |||
| e307ded192 | |||
| 2d6c997b38 | |||
| 232a80a848 | |||
| 083f8faa46 | |||
| 0fcf978ffe | |||
| c1360267c6 | |||
| 084bea6b15 | |||
| 2032dd88ba | |||
| b11b1be432 | |||
| b743b34fab | |||
| 950d10091d | |||
| af0e02b9a2 | |||
| 1332147c4a | |||
| f07cb1e7a3 | |||
| 53dbdd115f | |||
| a217ed5574 | |||
| 531f947754 | |||
| c957e9483e | |||
| 623a706555 | |||
| 7e171576e0 | |||
| 0979b3e03d | |||
| 1131bfa751 | |||
| f9b87b94bf | |||
| 59ed2ec87f | |||
| 7b0b79e3a1 | |||
| 53f73e1201 | |||
| c62a1dfff0 | |||
| 61f8055493 | |||
| 000d7fd249 | |||
| 087de03a1f | |||
| a3ca6159fb | |||
| 5de6ee136a | |||
| d5a19f2b42 | |||
| e3ec5dd506 | |||
| 762748225d | |||
| 4db34e0c56 | |||
| fb078d05bc | |||
| f59edffa43 | |||
| 7aa0ddb71f | |||
| a0a6256c7a | |||
| df7e331605 | |||
| 8c23704e17 | |||
| 12abb1731c | |||
| 180293ebc1 | |||
| e2af33e136 | |||
| 42e68edc65 | |||
| 47e732c213 | |||
| 77a86d92f4 | |||
| 64a8a046c1 | |||
| 1f02cbddd3 | |||
| 5e7bca02b3 | |||
| 097f9549b1 | |||
| 45434b16e0 | |||
| 6af5ac2be1 | |||
| 34ff7efa27 | |||
| 8f4391003f | |||
| ecefb30f3d | |||
| a8162b57ba | |||
| b0edac4234 | |||
| 98c4045a71 | |||
| 24e90e2ead | |||
| 145e0217b6 | |||
| e5925fb1b6 | |||
| 9e416d02bd | |||
| 82b7068130 | |||
| 579ee36857 | |||
| 4f2d7a519d | |||
| a3642d92c5 | |||
| 224f36164f | |||
| 638c220ae8 | |||
| 51070b3e7b | |||
| 0aa2723063 | |||
| 1af66c8e8b | |||
| 7df8795d52 | |||
| a0e9ae7092 | |||
| 0f0d8e317a | |||
| 3d5ca7d5c4 | |||
| e33104fa2b | |||
| a2f1723642 | |||
| 93357cf280 | |||
| 767427c787 | |||
| 9377631896 | |||
| d08af094b8 | |||
| c307b1e6fb | |||
| d387d5b758 | |||
| c285dd666d | |||
| b37b382ea7 | |||
| a2cd755ffa | |||
| 340aedfe13 | |||
| 6fafa7a75e | |||
| 03df5aaf42 | |||
| 6d84db08a8 | |||
| 1a5e0d2a5e | |||
| 70d887bada | |||
| ee0ac00f80 | |||
| fdfb07ff2c | |||
| b648155170 | |||
| 59dc5299b1 | |||
| 357a63a4d9 | |||
| 94912c7542 | |||
| fae182b328 | |||
| 9ba2f3e33a | |||
| 442687b198 | |||
| 7e400d3e9c |
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.gg/pJsjuNCKRq
|
||||||
|
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||||
|
- name: Matrix
|
||||||
|
url: https://matrix.to/#/#audiobookshelf:matrix.org
|
||||||
|
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||||
@@ -11,7 +11,6 @@ ExecReload=/bin/kill -HUP $MAINPID
|
|||||||
Restart=always
|
Restart=always
|
||||||
User=audiobookshelf
|
User=audiobookshelf
|
||||||
Group=audiobookshelf
|
Group=audiobookshelf
|
||||||
PermissionsStartOnly=true
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -20,42 +20,67 @@
|
|||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-enter-to, .slide-leave {
|
.slide-enter-to,
|
||||||
|
.slide-leave {
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-enter, .slide-leave-to {
|
.slide-enter,
|
||||||
|
.slide-leave-to {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.menu-enter, .menu-leave-active {
|
.menu-enter,
|
||||||
|
.menu-leave-active {
|
||||||
transform: translateY(-15px);
|
transform: translateY(-15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-enter-active {
|
.menu-enter-active {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-enter,
|
.menu-enter,
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter, .menux-leave-active {
|
.menux-enter,
|
||||||
|
.menux-leave-active {
|
||||||
transform: translateX(15px);
|
transform: translateX(15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter-active {
|
.menux-enter-active {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-leave-active {
|
.menux-leave-active {
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter,
|
.menux-enter,
|
||||||
.menux-leave-active {
|
.menux-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.list-complete-item {
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-complete-enter-from,
|
||||||
|
.list-complete-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-complete-leave-active {
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<div class="w-full h-16 bg-primary relative">
|
||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
@@ -18,22 +18,28 @@
|
|||||||
<widgets-notification-widget class="hidden md:block" />
|
<widgets-notification-widget class="hidden md:block" />
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
|
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
|
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
@@ -45,11 +51,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
<div v-show="numMediaItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||||
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numLibraryItemsSelected]) }}</h1>
|
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="!isPodcastLibrary" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||||
<span class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ $strings.ButtonPlay }}
|
{{ $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
@@ -62,7 +68,7 @@
|
|||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate">
|
<template v-if="userCanUpdate">
|
||||||
<ui-tooltip text="Edit" direction="bottom">
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -109,11 +115,14 @@ export default {
|
|||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : 'err'
|
return this.user ? this.user.username : 'err'
|
||||||
},
|
},
|
||||||
numLibraryItemsSelected() {
|
numMediaItemsSelected() {
|
||||||
return this.selectedLibraryItems.length
|
return this.selectedMediaItems.length
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems
|
return this.$store.state.globals.selectedMediaItems
|
||||||
|
},
|
||||||
|
selectedMediaItemsArePlayable() {
|
||||||
|
return !this.selectedMediaItems.some((i) => !i.hasTracks)
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.state.user.user.mediaProgress || []
|
return this.$store.state.user.user.mediaProgress || []
|
||||||
@@ -129,8 +138,8 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedLibraryItems.find((libraryItemId) => {
|
return !this.selectedMediaItems.find((item) => {
|
||||||
var itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === libraryItemId)
|
const itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === item.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress || !itemProgress.isFinished
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -154,12 +163,16 @@ export default {
|
|||||||
async playSelectedItems() {
|
async playSelectedItems() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
var libraryItems = await this.$axios.$post(`/api/items/batch/get`, { libraryItemIds: this.selectedLibraryItems }).catch((error) => {
|
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
||||||
var errorMsg = error.response.data || 'Failed to get items'
|
const libraryItems = await this.$axios
|
||||||
console.error(errorMsg, error)
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
this.$toast.error(errorMsg)
|
.then((res) => res.libraryItems)
|
||||||
return []
|
.catch((error) => {
|
||||||
})
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
|
console.error(errorMsg, error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
if (!libraryItems.length) {
|
if (!libraryItems.length) {
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
@@ -185,20 +198,20 @@ export default {
|
|||||||
queueItems
|
queueItems
|
||||||
})
|
})
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
},
|
},
|
||||||
cancelSelectionMode() {
|
cancelSelectionMode() {
|
||||||
if (this.processingBatch) return
|
if (this.processingBatch) return
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
},
|
},
|
||||||
toggleBatchRead() {
|
toggleBatchRead() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
var newIsFinished = !this.selectedIsFinished
|
const newIsFinished = !this.selectedIsFinished
|
||||||
var updateProgressPayloads = this.selectedLibraryItems.map((lid) => {
|
const updateProgressPayloads = this.selectedMediaItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
libraryItemId: lid,
|
libraryItemId: item.id,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -208,7 +221,7 @@ export default {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch update success!')
|
this.$toast.success('Batch update success!')
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -218,18 +231,18 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
batchDeleteClick() {
|
batchDeleteClick() {
|
||||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} items` : 'this item'
|
const audiobookText = this.numMediaItemsSelected > 1 ? `these ${this.numMediaItemsSelected} items` : 'this item'
|
||||||
var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
|
const confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/batch/delete`, {
|
.$post(`/api/items/batch/delete`, {
|
||||||
libraryItemIds: this.selectedLibraryItems
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch delete success!')
|
this.$toast.success('Batch delete success!')
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ export default {
|
|||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.bookCoverWidth / baseSize
|
return this.bookCoverWidth / baseSize
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -100,15 +100,15 @@ export default {
|
|||||||
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
||||||
|
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
this.lastItemIndexSelected = indexOf
|
this.lastItemIndexSelected = indexOf
|
||||||
} else {
|
} else {
|
||||||
this.lastItemIndexSelected = -1
|
this.lastItemIndexSelected = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||||
var loopStart = indexOf
|
let loopStart = indexOf
|
||||||
var loopEnd = lastLastItemIndexSelected
|
let loopEnd = lastLastItemIndexSelected
|
||||||
if (indexOf > lastLastItemIndexSelected) {
|
if (indexOf > lastLastItemIndexSelected) {
|
||||||
loopStart = lastLastItemIndexSelected
|
loopStart = lastLastItemIndexSelected
|
||||||
loopEnd = indexOf
|
loopEnd = indexOf
|
||||||
@@ -117,12 +117,12 @@ export default {
|
|||||||
const flattenedEntitiesArray = []
|
const flattenedEntitiesArray = []
|
||||||
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
|
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
|
||||||
|
|
||||||
var isSelecting = false
|
let isSelecting = false
|
||||||
// If any items in this range is not selected then select all otherwise unselect all
|
// If any items in this range is not selected then select all otherwise unselect all
|
||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = flattenedEntitiesArray[i]
|
const thisEntity = flattenedEntitiesArray[i]
|
||||||
if (thisEntity) {
|
if (thisEntity) {
|
||||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -133,13 +133,23 @@ export default {
|
|||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = flattenedEntitiesArray[i]
|
const thisEntity = flattenedEntitiesArray[i]
|
||||||
if (thisEntity) {
|
if (thisEntity) {
|
||||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
const mediaItem = {
|
||||||
|
id: thisEntity.id,
|
||||||
|
mediaType: thisEntity.mediaType,
|
||||||
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
const mediaItem = {
|
||||||
|
id: entity.id,
|
||||||
|
mediaType: entity.mediaType,
|
||||||
|
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -395,8 +405,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('user_updated', this.userUpdated)
|
this.$root.socket.off('user_updated', this.userUpdated)
|
||||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -119,14 +119,14 @@ export default {
|
|||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
} else if (this.shelf.type === 'episode') {
|
} else if (this.shelf.type === 'episode') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<!-- Series books page -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 font-book text-base md:text-lg">
|
<p class="pl-2 font-book text-base md:text-lg">
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||||
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||||
|
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -114,28 +114,30 @@ export default {
|
|||||||
totalEntities: 0,
|
totalEntities: 0,
|
||||||
processingSeries: false,
|
processingSeries: false,
|
||||||
processingIssues: false,
|
processingIssues: false,
|
||||||
processingAuthors: false,
|
processingAuthors: false
|
||||||
seriesSortItems: [
|
|
||||||
{
|
|
||||||
text: 'Name',
|
|
||||||
value: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Number of Books',
|
|
||||||
value: 'numBooks'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Date Added',
|
|
||||||
value: 'addedAt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Total Duration',
|
|
||||||
value: 'totalDuration'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
seriesSortItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelName,
|
||||||
|
value: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelNumberOfBooks,
|
||||||
|
value: 'numBooks'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAddedAt,
|
||||||
|
value: 'addedAt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTotalDuration,
|
||||||
|
value: 'totalDuration'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
@@ -163,6 +165,9 @@ export default {
|
|||||||
isCollectionsPage() {
|
isCollectionsPage() {
|
||||||
return this.page === 'collections'
|
return this.page === 'collections'
|
||||||
},
|
},
|
||||||
|
isPlaylistsPage() {
|
||||||
|
return this.page === 'playlists'
|
||||||
|
},
|
||||||
isHomePage() {
|
isHomePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
@@ -183,6 +188,7 @@ export default {
|
|||||||
if (!this.page) return this.$strings.LabelBooks
|
if (!this.page) return this.$strings.LabelBooks
|
||||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||||
|
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
@@ -199,7 +205,7 @@ export default {
|
|||||||
return this.seriesProgress.libraryItemIds || []
|
return this.seriesProgress.libraryItemIds || []
|
||||||
},
|
},
|
||||||
isBatchSelecting() {
|
isBatchSelecting() {
|
||||||
return this.$store.state.selectedLibraryItems.length
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
},
|
},
|
||||||
isSeriesFinished() {
|
isSeriesFinished() {
|
||||||
return this.seriesProgress && !!this.seriesProgress.isFinished
|
return this.seriesProgress && !!this.seriesProgress.isFinished
|
||||||
@@ -213,30 +219,6 @@ export default {
|
|||||||
},
|
},
|
||||||
isIssuesFilter() {
|
isIssuesFilter() {
|
||||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||||
},
|
|
||||||
seriesSortBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortBy', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesSortDesc: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortDesc', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesFilterBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesFilterBy', val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -333,10 +315,10 @@ export default {
|
|||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesSort() {
|
updateSeriesSort() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesFilter() {
|
updateSeriesFilter() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateCollapseSeries() {
|
updateCollapseSeries() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
@@ -361,11 +343,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export default {
|
|||||||
id: 'config-notifications',
|
id: 'config-notifications',
|
||||||
title: this.$strings.HeaderNotifications,
|
title: this.$strings.HeaderNotifications,
|
||||||
path: '/config/notifications'
|
path: '/config/notifications'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-item-metadata-utils',
|
||||||
|
title: this.$strings.HeaderItemMetadataUtils,
|
||||||
|
path: '/config/item-metadata-utils'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ export default {
|
|||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||||
|
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||||
return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue])
|
return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue])
|
||||||
// return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
|
||||||
}
|
}
|
||||||
return this.$strings.MessageNoResults
|
return this.$strings.MessageNoResults
|
||||||
},
|
},
|
||||||
@@ -100,13 +100,13 @@ export default {
|
|||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
seriesSortBy() {
|
seriesSortBy() {
|
||||||
return this.$store.state.libraries.seriesSortBy
|
return this.$store.getters['user/getUserSetting']('seriesSortBy')
|
||||||
},
|
},
|
||||||
seriesSortDesc() {
|
seriesSortDesc() {
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
return this.$store.getters['user/getUserSetting']('seriesSortDesc')
|
||||||
},
|
},
|
||||||
seriesFilterBy() {
|
seriesFilterBy() {
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
@@ -163,11 +163,11 @@ export default {
|
|||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 1.6
|
return this.bookWidth * 1.6
|
||||||
},
|
},
|
||||||
shelfPadding() {
|
shelfPadding() {
|
||||||
@@ -201,8 +201,8 @@ export default {
|
|||||||
// Includes margin
|
// Includes margin
|
||||||
return this.entityWidth + 24
|
return this.entityWidth + 24
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
@@ -220,6 +220,8 @@ export default {
|
|||||||
this.$store.commit('showEditModal', entity)
|
this.$store.commit('showEditModal', entity)
|
||||||
} else if (this.entityName === 'collections') {
|
} else if (this.entityName === 'collections') {
|
||||||
this.$store.commit('globals/setEditCollection', entity)
|
this.$store.commit('globals/setEditCollection', entity)
|
||||||
|
} else if (this.entityName === 'playlists') {
|
||||||
|
this.$store.commit('globals/setEditPlaylist', entity)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
@@ -228,28 +230,28 @@ export default {
|
|||||||
},
|
},
|
||||||
selectEntity(entity, shiftKey) {
|
selectEntity(entity, shiftKey) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
this.lastItemIndexSelected = indexOf
|
this.lastItemIndexSelected = indexOf
|
||||||
} else {
|
} else {
|
||||||
this.lastItemIndexSelected = -1
|
this.lastItemIndexSelected = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||||
var loopStart = indexOf
|
let loopStart = indexOf
|
||||||
var loopEnd = lastLastItemIndexSelected
|
let loopEnd = lastLastItemIndexSelected
|
||||||
if (indexOf > lastLastItemIndexSelected) {
|
if (indexOf > lastLastItemIndexSelected) {
|
||||||
loopStart = lastLastItemIndexSelected
|
loopStart = lastLastItemIndexSelected
|
||||||
loopEnd = indexOf
|
loopEnd = indexOf
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSelecting = false
|
let isSelecting = false
|
||||||
// If any items in this range is not selected then select all otherwise unselect all
|
// If any items in this range is not selected then select all otherwise unselect all
|
||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = this.entities[i]
|
const thisEntity = this.entities[i]
|
||||||
if (thisEntity && !thisEntity.collapsedSeries) {
|
if (thisEntity && !thisEntity.collapsedSeries) {
|
||||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -267,16 +269,28 @@ export default {
|
|||||||
const entityComponentRef = this.entityComponentRefs[i]
|
const entityComponentRef = this.entityComponentRefs[i]
|
||||||
if (thisEntity && entityComponentRef) {
|
if (thisEntity && entityComponentRef) {
|
||||||
entityComponentRef.selected = isSelecting
|
entityComponentRef.selected = isSelecting
|
||||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
|
||||||
|
const mediaItem = {
|
||||||
|
id: thisEntity.id,
|
||||||
|
mediaType: thisEntity.mediaType,
|
||||||
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
|
}
|
||||||
|
console.log('Setting media item selected', mediaItem, 'Num Selected=', this.selectedMediaItems.length)
|
||||||
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
const mediaItem = {
|
||||||
|
id: entity.id,
|
||||||
|
mediaType: entity.mediaType,
|
||||||
|
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newIsSelectionMode = !!this.selectedLibraryItems.length
|
const newIsSelectionMode = !!this.selectedMediaItems.length
|
||||||
if (this.isSelectionMode !== newIsSelectionMode) {
|
if (this.isSelectionMode !== newIsSelectionMode) {
|
||||||
this.isSelectionMode = newIsSelectionMode
|
this.isSelectionMode = newIsSelectionMode
|
||||||
this.updateBookSelectionMode(newIsSelectionMode)
|
this.updateBookSelectionMode(newIsSelectionMode)
|
||||||
@@ -302,11 +316,11 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||||
|
|
||||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -484,7 +498,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
var wasUpdated = this.checkUpdateSearchParams()
|
const wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
||||||
@@ -561,6 +575,33 @@ export default {
|
|||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
playlistAdded(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistAdded ${playlist.id}`, playlist)
|
||||||
|
this.resetEntities()
|
||||||
|
},
|
||||||
|
playlistUpdated(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistUpdated ${playlist.id}`, playlist)
|
||||||
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities[indexOf] = playlist
|
||||||
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
|
this.entityComponentRefs[indexOf].setEntity(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playlistRemoved(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistRemoved ${playlist.id}`, playlist)
|
||||||
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities = this.entities.filter((ent) => ent.id !== playlist.id)
|
||||||
|
this.totalEntities--
|
||||||
|
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||||
|
this.executeRebuild()
|
||||||
|
}
|
||||||
|
},
|
||||||
initSizeData(_bookshelf) {
|
initSizeData(_bookshelf) {
|
||||||
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
||||||
if (!bookshelf) {
|
if (!bookshelf) {
|
||||||
@@ -626,11 +667,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('socket_init', this.socketInit)
|
this.$eventBus.$on('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
@@ -641,6 +680,9 @@ export default {
|
|||||||
this.$root.socket.on('collection_added', this.collectionAdded)
|
this.$root.socket.on('collection_added', this.collectionAdded)
|
||||||
this.$root.socket.on('collection_updated', this.collectionUpdated)
|
this.$root.socket.on('collection_updated', this.collectionUpdated)
|
||||||
this.$root.socket.on('collection_removed', this.collectionRemoved)
|
this.$root.socket.on('collection_removed', this.collectionRemoved)
|
||||||
|
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||||
|
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
|
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
@@ -652,11 +694,9 @@ export default {
|
|||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('socket_init', this.socketInit)
|
this.$eventBus.$off('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
@@ -667,6 +707,9 @@ export default {
|
|||||||
this.$root.socket.off('collection_added', this.collectionAdded)
|
this.$root.socket.off('collection_added', this.collectionAdded)
|
||||||
this.$root.socket.off('collection_updated', this.collectionUpdated)
|
this.$root.socket.off('collection_updated', this.collectionUpdated)
|
||||||
this.$root.socket.off('collection_removed', this.collectionRemoved)
|
this.$root.socket.off('collection_removed', this.collectionRemoved)
|
||||||
|
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||||
|
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||||
|
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<h1 class="text-xl">{{ headerText }}</h1>
|
||||||
|
|
||||||
|
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
||||||
|
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" />
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
headerText: String,
|
||||||
|
description: String,
|
||||||
|
note: String,
|
||||||
|
showAddButton: Boolean
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clicked() {
|
||||||
|
this.$emit('clicked')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#settings-description a {
|
||||||
|
color: rgb(96 165 250);
|
||||||
|
}
|
||||||
|
#settings-description a:hover {
|
||||||
|
color: rgb(147 197 253);
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
#settings-description code {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgb(82, 82, 82);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- <div class="w-20 bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px"> -->
|
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||||
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-40" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
|
||||||
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
||||||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons">format_list_bulleted</span>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined">collections_bookmark</span>
|
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
|
|
||||||
@@ -71,6 +70,14 @@
|
|||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
|
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-icons text-2xl">warning</span>
|
||||||
|
|
||||||
@@ -143,6 +150,9 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.$route.name === 'library-library-authors'
|
||||||
},
|
},
|
||||||
|
isPlaylistsPage() {
|
||||||
|
return this.paramId === 'playlists'
|
||||||
|
},
|
||||||
libraryBookshelfPage() {
|
libraryBookshelfPage() {
|
||||||
return this.$route.name === 'library-library-bookshelf-id'
|
return this.$route.name === 'library-library-bookshelf-id'
|
||||||
},
|
},
|
||||||
@@ -173,6 +183,9 @@ export default {
|
|||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
showPlaylists() {
|
||||||
|
return this.$store.state.libraries.numUserPlaylists > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||||
|
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<player-ui
|
<player-ui
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
@@ -297,6 +299,16 @@ export default {
|
|||||||
this.playerHandler.seek(e.seekTime)
|
this.playerHandler.seek(e.seekTime)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mediaSessionPreviousTrack() {
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.prevChapter()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mediaSessionNextTrack() {
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.nextChapter()
|
||||||
|
}
|
||||||
|
},
|
||||||
updateMediaSessionPlaybackState() {
|
updateMediaSessionPlaybackState() {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
||||||
@@ -330,8 +342,9 @@ export default {
|
|||||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||||
// navigator.mediaSession.setActionHandler('previoustrack')
|
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionPreviousTrack)
|
||||||
// navigator.mediaSession.setActionHandler('nexttrack')
|
const hasNextChapter = this.$refs.audioPlayer && this.$refs.audioPlayer.hasNextChapter
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', hasNextChapter ? this.mediaSessionNextTrack : null)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Media session not available')
|
console.warn('Media session not available')
|
||||||
}
|
}
|
||||||
@@ -365,7 +378,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamReady() {
|
streamReady() {
|
||||||
console.log(`[STREAM-CONTAINER] Stream Ready`)
|
console.log(`[StreamContainer] Stream Ready`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setStreamReady()
|
this.$refs.audioPlayer.setStreamReady()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,10 +13,14 @@
|
|||||||
|
|
||||||
<!-- Search icon btn -->
|
<!-- Search icon btn -->
|
||||||
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
||||||
<span class="material-icons text-lg">search</span>
|
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
|
<span class="material-icons text-lg">search</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
||||||
<span class="material-icons text-lg">edit</span>
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
|
<span class="material-icons text-lg">edit</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
|
|||||||
@@ -410,6 +410,10 @@ export default {
|
|||||||
{
|
{
|
||||||
func: 'toggleFinished',
|
func: 'toggleFinished',
|
||||||
text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
|
text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func: 'openPlaylists',
|
||||||
|
text: this.$strings.LabelAddToPlaylist
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
@@ -448,6 +452,12 @@ export default {
|
|||||||
text: this.$strings.LabelAddToCollection
|
text: this.$strings.LabelAddToCollection
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.numTracks) {
|
||||||
|
items.push({
|
||||||
|
func: 'openPlaylists',
|
||||||
|
text: this.$strings.LabelAddToPlaylist
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -739,6 +749,10 @@ export default {
|
|||||||
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.store.commit('globals/setShowCollectionsModal', true)
|
this.store.commit('globals/setShowCollectionsModal', true)
|
||||||
},
|
},
|
||||||
|
openPlaylists() {
|
||||||
|
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
|
||||||
|
this.store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
createMoreMenu() {
|
createMoreMenu() {
|
||||||
if (!this.$refs.moreIcon) return
|
if (!this.$refs.moreIcon) return
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
|
</div>
|
||||||
|
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||||
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
playlistMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playlist: null,
|
||||||
|
isSelectionMode: false,
|
||||||
|
selected: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
|
return this.width / 240
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist ? this.playlist.items || [] : []
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
isAlternativeBookshelfView() {
|
||||||
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(playlist) {
|
||||||
|
this.playlist = playlist
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.playlist) return
|
||||||
|
var router = this.$router || this.$nuxt.$router
|
||||||
|
router.push(`/playlist/${this.playlist.id}`)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.playlist)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.playlistMount) {
|
||||||
|
this.setEntity(this.playlistMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons">arrow_right</span>
|
<span class="material-icons text-2xl">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons">arrow_left</span>
|
<span class="material-icons text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate">Back</span>
|
<span class="font-normal ml-3 block truncate">Back</span>
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('No Series'))">
|
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('no-series'))">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">No Series</span>
|
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">{{ $strings.MessageNoSeries }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in sublistItems">
|
<template v-for="item in sublistItems">
|
||||||
@@ -174,6 +174,11 @@ export default {
|
|||||||
value: 'missing',
|
value: 'missing',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTracks,
|
||||||
|
value: 'tracks',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonIssues,
|
text: this.$strings.ButtonIssues,
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
@@ -263,10 +268,92 @@ export default {
|
|||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return [this.$strings.LabelFinished, this.$strings.LabelInProgress, this.$strings.LabelNotStarted, this.$strings.LabelNotFinished]
|
return [
|
||||||
|
{
|
||||||
|
id: 'finished',
|
||||||
|
name: this.$strings.LabelFinished
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in-progress',
|
||||||
|
name: this.$strings.LabelInProgress
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-started',
|
||||||
|
name: this.$strings.LabelNotStarted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-finished',
|
||||||
|
name: this.$strings.LabelNotFinished
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'single',
|
||||||
|
name: this.$strings.LabelTracksSingleTrack
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multi',
|
||||||
|
name: this.$strings.LabelTracksMultiTrack
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return ['ASIN', 'ISBN', this.$strings.LabelSubtitle, this.$strings.LabelAuthor, this.$strings.LabelPublishYear, this.$strings.LabelSeries, this.$strings.LabelDescription, this.$strings.LabelGenres, this.$strings.LabelTags, this.$strings.LabelNarrator, this.$strings.LabelPublisher, this.$strings.LabelLanguage]
|
return [
|
||||||
|
{
|
||||||
|
id: 'asin',
|
||||||
|
name: 'ASIN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'isbn',
|
||||||
|
name: 'ISBN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle',
|
||||||
|
name: this.$strings.LabelSubtitle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'authors',
|
||||||
|
name: this.$strings.LabelAuthor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'publishedYear',
|
||||||
|
name: this.$strings.LabelPublishYear
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'series',
|
||||||
|
name: this.$strings.LabelSeries
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'description',
|
||||||
|
name: this.$strings.LabelDescription
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'genres',
|
||||||
|
name: this.$strings.LabelGenres
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
name: this.$strings.LabelTags
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'narrators',
|
||||||
|
name: this.$strings.LabelNarrator
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'publisher',
|
||||||
|
name: this.$strings.LabelPublisher
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'language',
|
||||||
|
name: this.$strings.LabelLanguage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cover',
|
||||||
|
name: this.$strings.LabelCover
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / (120 * 1.6 * 2)
|
||||||
|
},
|
||||||
|
itemCoverWidth() {
|
||||||
|
if (this.libraryItemCovers.length === 1) return this.width
|
||||||
|
return this.width / 2
|
||||||
|
},
|
||||||
|
libraryItemCovers() {
|
||||||
|
if (!this.items.length) return []
|
||||||
|
if (this.items.length === 1) return [this.items[0].libraryItem]
|
||||||
|
|
||||||
|
const covers = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let index = i % this.items.length
|
||||||
|
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
|
||||||
|
|
||||||
|
covers.push(this.items[index].libraryItem)
|
||||||
|
}
|
||||||
|
return covers
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -201,8 +201,8 @@ export default {
|
|||||||
this.loadingTags = true
|
this.loadingTags = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/tags`)
|
.$get(`/api/tags`)
|
||||||
.then((tags) => {
|
.then((res) => {
|
||||||
this.tags = tags
|
this.tags = res.tags
|
||||||
this.loadingTags = false
|
this.loadingTags = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateCover }}
|
{{ $strings.LabelUpdateCover }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateDetails }}
|
{{ $strings.LabelUpdateDetails }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ export default {
|
|||||||
return this.$store.state.globals.showBatchQuickMatchModal
|
return this.$store.state.globals.showBatchQuickMatchModal
|
||||||
},
|
},
|
||||||
selectedBookIds() {
|
selectedBookIds() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">add</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">add</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 50
|
default: 60
|
||||||
},
|
},
|
||||||
bgOpacity: {
|
bgOpacity: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<div class="flex pt-2 px-2">
|
<div class="flex pt-2 px-2">
|
||||||
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +136,7 @@ export default {
|
|||||||
})
|
})
|
||||||
if (result && result.updated) {
|
if (result && result.updated) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', result.author)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
@@ -157,7 +158,10 @@ export default {
|
|||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error('Author not found')
|
this.$toast.error('Author not found')
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
if (response.author.imagePath) {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', response.author)
|
||||||
|
}
|
||||||
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info('No updates were made for Author')
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="flex-grow pr-2">
|
<div class="flex-grow pr-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">forward</span></ui-btn>
|
||||||
<div class="pl-2 flex items-center">
|
<div class="pl-2 flex items-center">
|
||||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
<transition-group name="list-complete" tag="div">
|
<transition-group name="list-complete" tag="div">
|
||||||
<template v-for="collection in sortedCollections">
|
<template v-for="collection in sortedCollections">
|
||||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
<modals-collections-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@ export default {
|
|||||||
return this.$store.state.globals.showBatchCollectionModal
|
return this.$store.state.globals.showBatchCollectionModal
|
||||||
},
|
},
|
||||||
selectedBookIds() {
|
selectedBookIds() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
@@ -112,23 +112,21 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadCollections() {
|
loadCollections() {
|
||||||
if (!this.collections.length) {
|
this.processing = true
|
||||||
this.processing = true
|
this.$axios
|
||||||
this.$axios
|
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
.then((data) => {
|
||||||
.then((data) => {
|
if (data.results) {
|
||||||
if (data.results) {
|
this.$store.commit('libraries/setCollections', data.results || [])
|
||||||
this.$store.commit('libraries/setCollections', data.results || [])
|
}
|
||||||
}
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
.catch((error) => {
|
console.error('Failed to get collections', error)
|
||||||
console.error('Failed to get collections', error)
|
this.$toast.error('Failed to load collections')
|
||||||
this.$toast.error('Failed to load collections')
|
})
|
||||||
})
|
.finally(() => {
|
||||||
.finally(() => {
|
this.processing = false
|
||||||
this.processing = false
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
removeFromCollection(collection) {
|
removeFromCollection(collection) {
|
||||||
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
||||||
@@ -231,19 +229,3 @@ export default {
|
|||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.list-complete-item {
|
|
||||||
transition: all 0.8s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-complete-enter-from,
|
|
||||||
.list-complete-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-complete-leave-active {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
|
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
|
<div class="w-20 max-w-20 text-center">
|
||||||
|
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
|
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
|
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||||
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isBookIncluded() {
|
||||||
|
return !!this.collection.isBookIncluded
|
||||||
|
},
|
||||||
|
books() {
|
||||||
|
return this.collection.books || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickNuxtLink() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickAdd() {
|
||||||
|
this.$emit('add', this.collection)
|
||||||
|
},
|
||||||
|
clickRem() {
|
||||||
|
this.$emit('remove', this.collection)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
|
||||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
|
||||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
|
||||||
<div class="w-20 max-w-20 text-center">
|
|
||||||
<!-- <img src="/Logo.png" /> -->
|
|
||||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow overflow-hidden px-2">
|
|
||||||
<!-- <template v-if="isEditing">
|
|
||||||
<form @submit.prevent="submitUpdate">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex-grow pr-2">
|
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
|
||||||
<div class="pl-2 flex items-center">
|
|
||||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template> -->
|
|
||||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
|
||||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons pt-px">add</span></ui-btn>
|
|
||||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons pt-px">remove</span></ui-btn>
|
|
||||||
<!-- <span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
|
||||||
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
collection: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
highlight: Boolean,
|
|
||||||
bookCoverAspectRatio: Number
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isHovering: false,
|
|
||||||
isEditing: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isBookIncluded() {
|
|
||||||
return !!this.collection.isBookIncluded
|
|
||||||
},
|
|
||||||
wrapperClass() {
|
|
||||||
var classes = []
|
|
||||||
if (this.highlight) classes.push('bg-bg bg-opacity-60')
|
|
||||||
if (!this.isEditing) classes.push('cursor-pointer')
|
|
||||||
return classes.join(' ')
|
|
||||||
},
|
|
||||||
books() {
|
|
||||||
return this.collection.books || []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickNuxtLink() {
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
mouseover() {
|
|
||||||
if (this.isEditing) return
|
|
||||||
this.isHovering = true
|
|
||||||
},
|
|
||||||
mouseleave() {
|
|
||||||
this.isHovering = false
|
|
||||||
},
|
|
||||||
clickAdd() {
|
|
||||||
this.$emit('add', this.collection)
|
|
||||||
},
|
|
||||||
clickRem() {
|
|
||||||
this.$emit('remove', this.collection)
|
|
||||||
},
|
|
||||||
deleteClick() {
|
|
||||||
if (this.isEditing) return
|
|
||||||
this.$emit('delete', this.collection)
|
|
||||||
},
|
|
||||||
editClick() {
|
|
||||||
this.isEditing = true
|
|
||||||
this.isHovering = false
|
|
||||||
},
|
|
||||||
cancelEditing() {
|
|
||||||
this.isEditing = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||||
<span class="material-icons">delete</span>
|
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||||
|
<span class="material-icons text-2xl">delete</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
||||||
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
||||||
><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input
|
><span class="material-icons text-2xl inline-block md:!hidden">upload</span></ui-file-input
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||||
@@ -301,11 +303,14 @@ export default {
|
|||||||
this.persistProvider()
|
this.persistProvider()
|
||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
var results = await this.$axios.$get(`/api/search/covers?${searchQuery}`).catch((error) => {
|
const results = await this.$axios
|
||||||
console.error('Failed', error)
|
.$get(`/api/search/covers?${searchQuery}`)
|
||||||
return []
|
.then((res) => res.results)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.coversFound = results
|
this.coversFound = results
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
|
|||||||
@@ -306,13 +306,13 @@ export default {
|
|||||||
this.runSearch()
|
this.runSearch()
|
||||||
},
|
},
|
||||||
async runSearch() {
|
async runSearch() {
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
if (this.lastSearch === searchQuery) return
|
if (this.lastSearch === searchQuery) return
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
const searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
let results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -335,8 +335,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
},
|
},
|
||||||
init() {
|
initSelectedMatchUsage() {
|
||||||
this.clearSelectedMatch()
|
|
||||||
this.selectedMatchUsage = {
|
this.selectedMatchUsage = {
|
||||||
title: true,
|
title: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
@@ -360,6 +359,27 @@ export default {
|
|||||||
releaseDate: true
|
releaseDate: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load saved selected match from local storage
|
||||||
|
try {
|
||||||
|
let savedSelectedMatchUsage = localStorage.getItem('selectedMatchUsage')
|
||||||
|
if (!savedSelectedMatchUsage) return
|
||||||
|
savedSelectedMatchUsage = JSON.parse(savedSelectedMatchUsage)
|
||||||
|
|
||||||
|
for (const key in savedSelectedMatchUsage) {
|
||||||
|
if (this.selectedMatchUsage[key] !== undefined) {
|
||||||
|
this.selectedMatchUsage[key] = !!savedSelectedMatchUsage[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load saved selectedMatchUsage', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkboxToggled()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.clearSelectedMatch()
|
||||||
|
this.initSelectedMatchUsage()
|
||||||
|
|
||||||
if (this.libraryItem.id !== this.libraryItemId) {
|
if (this.libraryItem.id !== this.libraryItemId) {
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.hasSearched = false
|
this.hasSearched = false
|
||||||
@@ -465,11 +485,14 @@ export default {
|
|||||||
console.log('Match payload', updatePayload)
|
console.log('Match payload', updatePayload)
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
|
// Persist in local storage
|
||||||
|
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
|
||||||
|
|
||||||
if (updatePayload.metadata.cover) {
|
if (updatePayload.metadata.cover) {
|
||||||
var coverPayload = {
|
const coverPayload = {
|
||||||
url: updatePayload.metadata.cover
|
url: updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -483,8 +506,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
var mediaUpdatePayload = updatePayload
|
const mediaUpdatePayload = updatePayload
|
||||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -502,6 +525,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
clearSelectedMatch() {
|
clearSelectedMatch() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max episodes to keep
|
Max episodes to keep
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
Max new episodes to download per check
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-2 mb-4">
|
<div class="w-full h-full md:px-4 py-2 mb-4">
|
||||||
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
<div v-if="!showDirectoryPicker" class="w-full h-full md:py-4">
|
||||||
<div class="flex flex-wrap md:flex-nowrap -mx-1">
|
<div class="flex flex-wrap md:flex-nowrap -mx-1 mb-2">
|
||||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full py-4">
|
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||||
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
@@ -140,3 +140,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 192px);
|
||||||
|
}
|
||||||
|
@media (max-device-width: 768px) {
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 292px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-2 md:px-4 w-full text-sm pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||||
<span class="material-icons text-success">play_arrow</span>
|
<span class="material-icons text-2xl text-success">play_arrow</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||||
<span class="material-icons text-error">close</span>
|
<span class="material-icons text-2xl text-error">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
|
<div v-if="show" class="w-full h-full">
|
||||||
|
<div class="py-4 px-4">
|
||||||
|
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||||
|
<h1 v-else class="text-2xl">{{ $getString('LabelAddToPlaylistBatch', [selectedPlaylistItems.length]) }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
|
<transition-group name="list-complete" tag="div">
|
||||||
|
<template v-for="playlist in sortedPlaylists">
|
||||||
|
<modals-playlists-user-playlist-item :key="playlist.id" :playlist="playlist" class="list-complete-item" @add="addToPlaylist" @remove="removeFromPlaylist" @close="show = false" />
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
|
||||||
|
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||||
|
<form @submit.prevent="submitCreatePlaylist">
|
||||||
|
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<ui-text-input v-model="newPlaylistName" :placeholder="$strings.PlaceholderNewPlaylist" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newPlaylistName: '',
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.loadPlaylists()
|
||||||
|
this.newPlaylistName = ''
|
||||||
|
} else {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showPlaylistsModal
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
if (!this.selectedPlaylistItems.length) return ''
|
||||||
|
if (this.isBatch) {
|
||||||
|
return this.$getString('MessageItemsSelected', [this.selectedPlaylistItems.length])
|
||||||
|
}
|
||||||
|
const selectedPlaylistItem = this.selectedPlaylistItems[0]
|
||||||
|
if (selectedPlaylistItem.episode) {
|
||||||
|
return selectedPlaylistItem.episode.title
|
||||||
|
}
|
||||||
|
return selectedPlaylistItem.libraryItem.media.metadata.title || ''
|
||||||
|
},
|
||||||
|
playlists() {
|
||||||
|
return this.$store.state.libraries.userPlaylists || []
|
||||||
|
},
|
||||||
|
sortedPlaylists() {
|
||||||
|
return this.playlists
|
||||||
|
.map((playlist) => {
|
||||||
|
const includesItem = !this.selectedPlaylistItems.some((item) => !this.checkIsItemInPlaylist(playlist, item))
|
||||||
|
|
||||||
|
return {
|
||||||
|
isItemIncluded: includesItem,
|
||||||
|
...playlist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.isItemIncluded ? -1 : 1))
|
||||||
|
},
|
||||||
|
isBatch() {
|
||||||
|
return this.selectedPlaylistItems.length > 1
|
||||||
|
},
|
||||||
|
selectedPlaylistItems() {
|
||||||
|
return this.$store.state.globals.selectedPlaylistItems || []
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkIsItemInPlaylist(playlist, item) {
|
||||||
|
if (item.episode) {
|
||||||
|
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id && i.episodeId === item.episode.id)
|
||||||
|
}
|
||||||
|
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id)
|
||||||
|
},
|
||||||
|
loadPlaylists() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/libraries/${this.currentLibraryId}/playlists`)
|
||||||
|
.then((data) => {
|
||||||
|
this.$store.commit('libraries/setUserPlaylists', data.results || [])
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to get playlists', error)
|
||||||
|
this.$toast.error('Failed to load user playlists')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeFromPlaylist(playlist) {
|
||||||
|
if (!this.selectedPlaylistItems.length) return
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Playlist item(s) removed')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove items from playlist', error)
|
||||||
|
this.$toast.error('Failed to remove playlist item(s)')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addToPlaylist(playlist) {
|
||||||
|
if (!this.selectedPlaylistItems.length) return
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
console.log(`Items added to playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Items added to playlist')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to add items to playlist', error)
|
||||||
|
this.$toast.error('Failed to add items to playlist')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitCreatePlaylist() {
|
||||||
|
if (!this.newPlaylistName || !this.selectedPlaylistItems.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
const newPlaylist = {
|
||||||
|
items: itemObjects,
|
||||||
|
libraryId: this.currentLibraryId,
|
||||||
|
name: this.newPlaylistName
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/playlists', newPlaylist)
|
||||||
|
.then((data) => {
|
||||||
|
console.log('New playlist created', data)
|
||||||
|
this.$toast.success(`Playlist "${data.name}" created`)
|
||||||
|
this.processing = false
|
||||||
|
this.newPlaylistName = ''
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to create playlist', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex">
|
||||||
|
<div>
|
||||||
|
<covers-playlist-cover :items="items" :width="200" :height="200" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-4">
|
||||||
|
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" class="mb-2" />
|
||||||
|
|
||||||
|
<ui-textarea-with-label v-model="newPlaylistDescription" :label="$strings.LabelDescription" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||||
|
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newPlaylistName: null,
|
||||||
|
newPlaylistDescription: null,
|
||||||
|
showImageUploader: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showEditPlaylistModal
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowEditPlaylistModal', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playlist() {
|
||||||
|
return this.$store.state.globals.selectedPlaylist || {}
|
||||||
|
},
|
||||||
|
playlistName() {
|
||||||
|
return this.playlist.name
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.newPlaylistName = this.playlistName
|
||||||
|
this.newPlaylistDescription = this.playlist.description || ''
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
if (confirm(this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]))) {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove playlist', error)
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.newPlaylistName) {
|
||||||
|
return this.$toast.error('Playlist must have a name')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
var playlistUpdate = {
|
||||||
|
name: this.newPlaylistName,
|
||||||
|
description: this.newPlaylistDescription || null
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/playlists/${this.playlist.id}`, playlistUpdate)
|
||||||
|
.then((playlist) => {
|
||||||
|
console.log('Playlist Updated', playlist)
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update playlist', error)
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
|
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
|
<div class="w-16 max-w-16 text-center">
|
||||||
|
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
|
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
|
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||||
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
playlist: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isItemIncluded() {
|
||||||
|
return !!this.playlist.isItemIncluded
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickNuxtLink() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickAdd() {
|
||||||
|
this.$emit('add', this.playlist)
|
||||||
|
},
|
||||||
|
clickRem() {
|
||||||
|
this.$emit('remove', this.playlist)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||||
|
|||||||
@@ -4,27 +4,37 @@
|
|||||||
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||||
|
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||||
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
|
||||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||||
<div v-else class="flex items-center">
|
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
||||||
<span class="material-icons text-lg text-warning">snooze</span>
|
<div v-else class="flex items-center">
|
||||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
<span class="material-icons text-lg text-warning">snooze</span>
|
||||||
|
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ui-tooltip>
|
||||||
|
|
||||||
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
|
||||||
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||||
</div>
|
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<div v-if="chapters.length" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
</div>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
||||||
<span class="material-icons text-2.5xl sm:text-3xl">queue_music</span>
|
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||||
</button>
|
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
||||||
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||||
@@ -224,13 +234,10 @@ export default {
|
|||||||
this.showChaptersModal = false
|
this.showChaptersModal = false
|
||||||
},
|
},
|
||||||
setUseChapterTrack() {
|
setUseChapterTrack() {
|
||||||
var useChapterTrack = !this.useChapterTrack
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
this.useChapterTrack = useChapterTrack
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(useChapterTrack)
|
|
||||||
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { useChapterTrack }).catch((err) => {
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||||
console.error('Failed to update settings', err)
|
|
||||||
})
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
},
|
},
|
||||||
checkUpdateChapterTrack() {
|
checkUpdateChapterTrack() {
|
||||||
@@ -301,7 +308,7 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
var _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
||||||
|
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
@@ -335,13 +342,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'audioplayer', meth: this.settingsUpdated })
|
|
||||||
this.init()
|
|
||||||
this.$eventBus.$on('player-hotkey', this.hotkey)
|
this.$eventBus.$on('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'audioplayer')
|
|
||||||
this.$eventBus.$off('player-hotkey', this.hotkey)
|
this.$eventBus.$off('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-primary text-white">
|
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 right-4 z-20">
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,13 +92,18 @@ export default {
|
|||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
if (!this.ebookFile) return null
|
if (!this.ebookFile) return null
|
||||||
var itemRelPath = this.selectedLibraryItem.relPath
|
let filepath = ''
|
||||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
if (this.selectedLibraryItem.isFile) {
|
||||||
var relPath = this.ebookFile.metadata.relPath
|
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
} else {
|
||||||
|
const itemRelPath = this.selectedLibraryItem.relPath
|
||||||
|
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||||
|
const relPath = this.ebookFile.metadata.relPath
|
||||||
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
const relRelPath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
filepath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${relRelPath}`
|
}
|
||||||
|
return `/ebook/${this.libraryId}/${this.folderId}/${filepath}`
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
||||||
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||||
<span class="material-icons-outlined text-error">error_outline</span>
|
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
||||||
@@ -64,13 +64,11 @@ export default {
|
|||||||
showConfirmApply: false,
|
showConfirmApply: false,
|
||||||
selectedBackup: null,
|
selectedBackup: null,
|
||||||
isBackingUp: false,
|
isBackingUp: false,
|
||||||
processing: false
|
processing: false,
|
||||||
|
backups: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
backups() {
|
|
||||||
return this.$store.state.backups || []
|
|
||||||
},
|
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
@@ -96,9 +94,8 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/backups/${backup.id}`)
|
.$delete(`/api/backups/${backup.id}`)
|
||||||
.then((backups) => {
|
.then((data) => {
|
||||||
console.log('Backup deleted', backups)
|
this.setBackups(data.backups || [])
|
||||||
this.$store.commit('setBackups', backups)
|
|
||||||
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
|
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
@@ -117,10 +114,10 @@ export default {
|
|||||||
this.isBackingUp = true
|
this.isBackingUp = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post('/api/backups')
|
.$post('/api/backups')
|
||||||
.then((backups) => {
|
.then((data) => {
|
||||||
this.isBackingUp = false
|
this.isBackingUp = false
|
||||||
this.$toast.success(this.$strings.ToastBackupCreateSuccess)
|
this.$toast.success(this.$strings.ToastBackupCreateSuccess)
|
||||||
this.$store.commit('setBackups', backups)
|
this.setBackups(data.backups || [])
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.isBackingUp = false
|
this.isBackingUp = false
|
||||||
@@ -136,9 +133,8 @@ export default {
|
|||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post('/api/backups/upload', form)
|
.$post('/api/backups/upload', form)
|
||||||
.then((result) => {
|
.then((data) => {
|
||||||
console.log('Upload backup result', result)
|
this.setBackups(data.backups || [])
|
||||||
this.$store.commit('setBackups', result)
|
|
||||||
this.$toast.success(this.$strings.ToastBackupUploadSuccess)
|
this.$toast.success(this.$strings.ToastBackupUploadSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
@@ -148,9 +144,29 @@ export default {
|
|||||||
this.$toast.error(errorMessage)
|
this.$toast.error(errorMessage)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
setBackups(backups) {
|
||||||
|
backups.sort((a, b) => b.createdAt - a.createdAt)
|
||||||
|
this.backups = backups
|
||||||
|
},
|
||||||
|
loadBackups() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/backups')
|
||||||
|
.then((data) => {
|
||||||
|
this.setBackups(data.backups || [])
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load backups', error)
|
||||||
|
this.$toast.error('Failed to load backups')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.loadBackups()
|
||||||
if (this.$route.query.backup) {
|
if (this.$route.query.backup) {
|
||||||
this.$toast.success('Backup applied successfully')
|
this.$toast.success('Backup applied successfully')
|
||||||
this.$router.replace('/config')
|
this.$router.replace('/config')
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full bg-primary bg-opacity-40">
|
||||||
|
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
|
||||||
|
<p class="pr-4">{{ $strings.HeaderPlaylistItems }}</p>
|
||||||
|
|
||||||
|
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||||
|
</div>
|
||||||
|
<draggable v-model="itemsCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
|
<transition-group type="transition" :name="!drag ? 'playlist-item' : null">
|
||||||
|
<template v-for="(item, index) in itemsCopy">
|
||||||
|
<tables-playlist-item-table-row :key="index" :is-dragging="drag" :item="item" :playlist-id="playlistId" :book-cover-aspect-ratio="bookCoverAspectRatio" class="item" :class="drag ? '' : 'playlist-item-item'" @edit="editItem" />
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
playlistId: String,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drag: false,
|
||||||
|
dragOptions: {
|
||||||
|
animation: 200,
|
||||||
|
group: 'description',
|
||||||
|
ghostClass: 'ghost'
|
||||||
|
},
|
||||||
|
itemsCopy: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
items: {
|
||||||
|
handler(newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
totalDuration() {
|
||||||
|
var _total = 0
|
||||||
|
this.items.forEach((item) => {
|
||||||
|
if (item.episode) _total += item.episode.duration
|
||||||
|
else _total += item.libraryItem.media.duration
|
||||||
|
})
|
||||||
|
return _total
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editItem(playlistItem) {
|
||||||
|
if (playlistItem.episode) {
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', playlist.episode)
|
||||||
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
} else {
|
||||||
|
const itemIds = this.items.map((i) => i.libraryItemId)
|
||||||
|
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||||
|
this.$store.commit('showEditModal', playlistItem.libraryItem)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
draggableUpdate() {
|
||||||
|
var playlistUpdate = {
|
||||||
|
items: this.itemsCopy.map((i) => ({ libraryItemId: i.libraryItemId, episodeId: i.episodeId }))
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/playlists/${this.playlistId}`, playlistUpdate)
|
||||||
|
.then((playlist) => {
|
||||||
|
console.log('Playlist updated', playlist)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update playlist', error)
|
||||||
|
this.$toast.error('Failed to save playlist items order')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.itemsCopy = this.items.map((i) => ({ ...i }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.playlist-item-item {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item-enter-from,
|
||||||
|
.playlist-item-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderUsers }}</h1>
|
|
||||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser">
|
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<table id="accounts">
|
<table id="accounts">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -26,11 +19,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-sm">{{ user.type }}</td>
|
<td class="text-sm">{{ user.type }}</td>
|
||||||
<td class="hidden lg:table-cell">
|
<td class="hidden lg:table-cell">
|
||||||
<div v-if="usersOnline[user.id] && usersOnline[user.id].session && usersOnline[user.id].session.libraryItem && usersOnline[user.id].session.libraryItem.media">
|
<div v-if="usersOnline[user.id]">
|
||||||
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
<p v-if="usersOnline[user.id].session && usersOnline[user.id].session.libraryItem" class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||||
</div>
|
<p v-else-if="usersOnline[user.id].mostRecent && usersOnline[user.id].mostRecent.media" class="truncate text-xs">Last: {{ usersOnline[user.id].mostRecent.media.metadata.title }}</p>
|
||||||
<div v-else-if="user.mostRecent">
|
|
||||||
<p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
@@ -81,7 +72,7 @@ export default {
|
|||||||
},
|
},
|
||||||
usersOnline() {
|
usersOnline() {
|
||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
||||||
return usermap
|
return usermap
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,8 +109,8 @@ export default {
|
|||||||
loadUsers() {
|
loadUsers() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
this.users = users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
<span class="material-icons">play_arrow</span>
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLibraries }}</h1>
|
|
||||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
|
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
@@ -88,10 +82,10 @@ export default {
|
|||||||
})
|
})
|
||||||
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||||
if (currOrder !== newOrder) {
|
if (currOrder !== newOrder) {
|
||||||
this.$axios.$post('/api/libraries/order', libraryOrderData).then((libraries) => {
|
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
||||||
if (libraries && libraries.length) {
|
if (response.libraries && response.libraries.length) {
|
||||||
this.$toast.success('Library order saved', { timeout: 1500 })
|
this.$toast.success('Library order saved', { timeout: 1500 })
|
||||||
this.$store.commit('libraries/set', libraries)
|
this.$store.commit('libraries/set', response.libraries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
|
<div v-if="item" class="flex h-16 md:h-20">
|
||||||
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden max-w-48 md:max-w-md h-full flex items-center px-2 md:px-3">
|
||||||
|
<div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md">
|
||||||
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="truncate hover:underline text-sm md:text-base">{{ itemTitle }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
|
<template v-for="(author, index) in bookAuthors">
|
||||||
|
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs md:text-sm text-gray-400">{{ itemDuration }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
|
||||||
|
<div class="flex h-full items-center">
|
||||||
|
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
|
</ui-tooltip>
|
||||||
|
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||||
|
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||||
|
</div>
|
||||||
|
<div v-if="userCanDelete" class="mx-1">
|
||||||
|
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
playlistId: String,
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
isDragging: Boolean,
|
||||||
|
bookCoverAspectRatio: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isProcessingReadUpdate: false,
|
||||||
|
processingRemove: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isDragging: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.isHovering = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
translateDistance() {
|
||||||
|
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
||||||
|
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
||||||
|
return '-translate-x-24'
|
||||||
|
},
|
||||||
|
libraryItem() {
|
||||||
|
return this.item.libraryItem || {}
|
||||||
|
},
|
||||||
|
episode() {
|
||||||
|
return this.item.episode
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
if (this.episode) return []
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
|
itemTitle() {
|
||||||
|
if (this.episode) return this.episode.title
|
||||||
|
return this.mediaMetadata.title || ''
|
||||||
|
},
|
||||||
|
bookAuthors() {
|
||||||
|
if (this.episode) return []
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
|
itemDuration() {
|
||||||
|
if (this.episode) return this.$elapsedPretty(this.episode.duration)
|
||||||
|
return this.$elapsedPretty(this.media.duration)
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.libraryItem.isMissing
|
||||||
|
},
|
||||||
|
isInvalid() {
|
||||||
|
return this.libraryItem.isInvalid
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.$store.getters['getIsMediaStreaming'](this.libraryItem.id, this.episodeId)
|
||||||
|
},
|
||||||
|
showPlayBtn() {
|
||||||
|
return !this.isMissing && !this.isInvalid && !this.isStreaming && (this.tracks.length || this.episode)
|
||||||
|
},
|
||||||
|
itemProgress() {
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
|
||||||
|
},
|
||||||
|
userIsFinished() {
|
||||||
|
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||||
|
},
|
||||||
|
coverSize() {
|
||||||
|
return this.$store.state.globals.isMobile ? 30 : 50
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
|
return this.coverSize
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mouseover() {
|
||||||
|
if (this.isDragging) return
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
playClick() {
|
||||||
|
let queueItem = null
|
||||||
|
if (this.episode) {
|
||||||
|
queueItem = {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
libraryId: this.libraryItem.libraryId,
|
||||||
|
episodeId: this.episodeId,
|
||||||
|
title: this.itemTitle,
|
||||||
|
subtitle: this.mediaMetadata.title,
|
||||||
|
caption: '',
|
||||||
|
duration: this.media.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueItem = {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
libraryId: this.libraryItem.libraryId,
|
||||||
|
episodeId: null,
|
||||||
|
title: this.itemTitle,
|
||||||
|
subtitle: this.bookAuthors.map((au) => au.name).join(', '),
|
||||||
|
caption: '',
|
||||||
|
duration: this.media.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episodeId: this.episodeId,
|
||||||
|
queueItems: [queueItem]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.item)
|
||||||
|
},
|
||||||
|
toggleFinished() {
|
||||||
|
var updatePayload = {
|
||||||
|
isFinished: !this.userIsFinished
|
||||||
|
}
|
||||||
|
this.isProcessingReadUpdate = true
|
||||||
|
|
||||||
|
let routepath = `/api/me/progress/${this.libraryItem.id}`
|
||||||
|
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$patch(routepath, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
this.processingRemove = true
|
||||||
|
|
||||||
|
let routepath = `/api/playlists/${this.playlistId}/item/${this.libraryItem.id}`
|
||||||
|
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(routepath)
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
if (!updatedPlaylist.items.length) {
|
||||||
|
console.log(`All items removed so playlist was removed`, updatedPlaylist)
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
|
} else {
|
||||||
|
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Item removed from playlist')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove item from playlist', error)
|
||||||
|
this.$toast.error('Failed to remove item from playlist')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processingRemove = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,18 +16,26 @@
|
|||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
||||||
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
||||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'playlist_add' }}</span>
|
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
||||||
</button>
|
</button> -->
|
||||||
|
|
||||||
|
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
||||||
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
|
||||||
|
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
@@ -123,6 +131,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickAddToPlaylist() {
|
||||||
|
this.$emit('addToPlaylist', this.episode)
|
||||||
|
},
|
||||||
clickedEpisode() {
|
clickedEpisode() {
|
||||||
this.$emit('view', this.episode)
|
this.$emit('view', this.episode)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<template v-for="episode in episodesSorted">
|
||||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||||
@@ -131,6 +131,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
addToPlaylist(episode) {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
addEpisodeToQueue(episode) {
|
addEpisodeToQueue(episode) {
|
||||||
const queueItem = {
|
const queueItem = {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
|
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
|
||||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
|
||||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
|
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="material-icons">more_vert</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="menu">
|
||||||
|
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
disabled: Boolean,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
clickAction(action) {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = false
|
||||||
|
this.$emit('action', action)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<span class="material-icons">expand_more</span>
|
<span class="material-icons text-2xl">expand_more</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<span class="block truncate">{{ label }}</span>
|
<span class="block truncate">{{ label }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<span class="material-icons text-gray-100" aria-label="User Account" role="button">person</span>
|
<span class="material-icons text-2xl text-gray-100" aria-label="User Account" role="button">person</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning mr-1" style="font-size: 1rem" @click.stop="editItem(item)">edit</span>
|
<span v-if="showEdit" class="material-icons text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
||||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
||||||
</div>
|
</div>
|
||||||
{{ item[textKey] }}
|
{{ item[textKey] }}
|
||||||
@@ -113,10 +113,13 @@ export default {
|
|||||||
if (this.searching) return
|
if (this.searching) return
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
this.searching = true
|
this.searching = true
|
||||||
var results = await this.$axios.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`).catch((error) => {
|
const results = await this.$axios
|
||||||
console.error('Failed to get search results', error)
|
.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`)
|
||||||
return []
|
.then((res) => res.results || res)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to get search results', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.items = results || []
|
this.items = results || []
|
||||||
this.searching = false
|
this.searching = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
tooltip: null,
|
tooltip: null,
|
||||||
tooltipId: null,
|
tooltipId: null,
|
||||||
isShowing: false
|
isShowing: false,
|
||||||
|
hideTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -46,10 +47,12 @@ export default {
|
|||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
||||||
tooltip.id = this.tooltipId
|
tooltip.id = this.tooltipId
|
||||||
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
|
tooltip.addEventListener('mouseover', this.cancelHide);
|
||||||
|
tooltip.addEventListener('mouseleave', this.hideTooltip);
|
||||||
|
|
||||||
this.setTooltipPosition(tooltip)
|
this.setTooltipPosition(tooltip)
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isShowing = true
|
this.isShowing = true
|
||||||
},
|
},
|
||||||
hideTooltip() {
|
hideTooltip() {
|
||||||
@@ -102,11 +106,16 @@ export default {
|
|||||||
this.tooltip.remove()
|
this.tooltip.remove()
|
||||||
this.isShowing = false
|
this.isShowing = false
|
||||||
},
|
},
|
||||||
|
cancelHide() {
|
||||||
|
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
||||||
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
if (!this.isShowing) this.showTooltip()
|
if (!this.isShowing) this.showTooltip()
|
||||||
},
|
},
|
||||||
mouseleave() {
|
mouseleave() {
|
||||||
if (this.isShowing) this.hideTooltip()
|
if (this.isShowing) {
|
||||||
|
this.hideTimeout = setTimeout(this.hideTooltip, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -137,16 +137,33 @@ export default {
|
|||||||
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
if (key === 'tags') {
|
if (mapType === 'append') {
|
||||||
this.newTags = [...batchDetails.tags]
|
if (key === 'tags') {
|
||||||
} else if (key === 'genres' || key === 'narrators') {
|
// Concat and remove dupes
|
||||||
this.details[key] = [...batchDetails[key]]
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
} else if (key === 'authors' || key === 'series') {
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
this.details[key] = batchDetails[key].map((i) => ({ ...i }))
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
} else if (key === 'authors' || key === 'series') {
|
||||||
|
batchDetails[key].forEach((detail) => {
|
||||||
|
const existingDetail = this.details[key].find((_d) => _d.name.toLowerCase() == detail.name.toLowerCase().trim() || _d.id == detail.id)
|
||||||
|
if (!existingDetail) {
|
||||||
|
this.details[key].push({ ...detail })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.details[key] = batchDetails[key]
|
if (key === 'tags') {
|
||||||
|
this.newTags = [...batchDetails.tags]
|
||||||
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
|
this.details[key] = [...batchDetails[key]]
|
||||||
|
} else if (key === 'authors' || key === 'series') {
|
||||||
|
this.details[key] = batchDetails[key].map((i) => ({ ...i }))
|
||||||
|
} else {
|
||||||
|
this.details[key] = batchDetails[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,35 +101,35 @@ export default {
|
|||||||
intervalOptions() {
|
intervalOptions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: 'Custom daily/weekly',
|
text: this.$strings.LabelIntervalCustomDailyWeekly,
|
||||||
value: 'custom'
|
value: 'custom'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every day',
|
text: this.$strings.LabelIntervalEveryDay,
|
||||||
value: 'daily'
|
value: 'daily'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 12 hours',
|
text: this.$strings.LabelIntervalEvery12Hours,
|
||||||
value: '0 */12 * * *'
|
value: '0 */12 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 6 hours',
|
text: this.$strings.LabelIntervalEvery6Hours,
|
||||||
value: '0 */6 * * *'
|
value: '0 */6 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 2 hours',
|
text: this.$strings.LabelIntervalEvery2Hours,
|
||||||
value: '0 */2 * * *'
|
value: '0 */2 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every hour',
|
text: this.$strings.LabelIntervalEveryHour,
|
||||||
value: '0 * * * *'
|
value: '0 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 30 minutes',
|
text: this.$strings.LabelIntervalEvery30Minutes,
|
||||||
value: '*/30 * * * *'
|
value: '*/30 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 15 minutes',
|
text: this.$strings.LabelIntervalEvery15Minutes,
|
||||||
value: '*/15 * * * *'
|
value: '*/15 * * * *'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -101,14 +101,14 @@ export default {
|
|||||||
this.updateSelectionMode(this.isSelectionMode)
|
this.updateSelectionMode(this.isSelectionMode)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
|
|
||||||
this.items.forEach((ent) => {
|
this.items.forEach((ent) => {
|
||||||
var component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
|
let component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -82,14 +82,14 @@ export default {
|
|||||||
this.updateSelectionMode(this.isSelectionMode)
|
this.updateSelectionMode(this.isSelectionMode)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
|
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
var component = this.$refs[`slider-item-${item.id}`]
|
let component = this.$refs[`slider-item-${item.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(item.id)
|
component.selected = selectedMediaItems.some((i) => i.id === item.id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
|
|||||||
@@ -107,14 +107,24 @@ export default {
|
|||||||
author: this.details.author
|
author: this.details.author
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
if (key === 'tags') {
|
if (mapType === 'append') {
|
||||||
this.newTags = [...batchDetails.tags]
|
if (key === 'tags') {
|
||||||
} else if (key === 'genres') {
|
// Concat and remove dupes
|
||||||
this.details[key] = [...batchDetails[key]]
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.details[key] = batchDetails[key]
|
if (key === 'tags') {
|
||||||
|
this.newTags = [...batchDetails.tags]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
this.details[key] = [...batchDetails[key]]
|
||||||
|
} else {
|
||||||
|
this.details[key] = batchDetails[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
+52
-22
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
<modals-item-edit-modal />
|
<modals-item-edit-modal />
|
||||||
<modals-collections-add-create-modal />
|
<modals-collections-add-create-modal />
|
||||||
<modals-edit-collection-modal />
|
<modals-collections-edit-modal />
|
||||||
|
<modals-playlists-add-create-modal />
|
||||||
|
<modals-playlists-edit-modal />
|
||||||
<modals-podcast-edit-episode />
|
<modals-podcast-edit-episode />
|
||||||
<modals-podcast-view-episode />
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
@@ -40,9 +42,8 @@ export default {
|
|||||||
if (this.$store.state.showEditModal) {
|
if (this.$store.state.showEditModal) {
|
||||||
this.$store.commit('setShowEditModal', false)
|
this.$store.commit('setShowEditModal', false)
|
||||||
}
|
}
|
||||||
if (this.$store.state.selectedLibraryItems) {
|
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
}
|
|
||||||
this.updateBodyClass()
|
this.updateBodyClass()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -53,9 +54,12 @@ export default {
|
|||||||
isCasting() {
|
isCasting() {
|
||||||
return this.$store.state.globals.isCasting
|
return this.$store.state.globals.isCasting
|
||||||
},
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
isShowingSideRail() {
|
isShowingSideRail() {
|
||||||
if (!this.$route.name) return false
|
if (!this.$route.name) return false
|
||||||
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
|
return !this.$route.name.startsWith('config') && this.currentLibraryId
|
||||||
},
|
},
|
||||||
isShowingToolbar() {
|
isShowingToolbar() {
|
||||||
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
||||||
@@ -132,14 +136,8 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (payload.backups && payload.backups.length) {
|
|
||||||
this.$store.commit('setBackups', payload.backups)
|
|
||||||
}
|
|
||||||
if (payload.usersOnline) {
|
if (payload.usersOnline) {
|
||||||
this.$store.commit('users/resetUsers')
|
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||||
payload.usersOnline.forEach((user) => {
|
|
||||||
this.$store.commit('users/updateUser', user)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$emit('socket_init')
|
this.$eventBus.$emit('socket_init')
|
||||||
@@ -174,7 +172,7 @@ export default {
|
|||||||
this.$store.commit('libraries/remove', library)
|
this.$store.commit('libraries/remove', library)
|
||||||
|
|
||||||
// When removed currently selected library then set next accessible library
|
// When removed currently selected library then set next accessible library
|
||||||
const currLibraryId = this.$store.state.libraries.currentLibraryId
|
const currLibraryId = this.currentLibraryId
|
||||||
if (currLibraryId === library.id) {
|
if (currLibraryId === library.id) {
|
||||||
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
||||||
if (nextLibrary) {
|
if (nextLibrary) {
|
||||||
@@ -213,7 +211,7 @@ export default {
|
|||||||
libraryItemRemoved(item) {
|
libraryItemRemoved(item) {
|
||||||
if (this.$route.name.startsWith('item')) {
|
if (this.$route.name.startsWith('item')) {
|
||||||
if (this.$route.params.id === item.id) {
|
if (this.$route.params.id === item.id) {
|
||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
this.$router.replace(`/library/${this.currentLibraryId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -282,35 +280,55 @@ export default {
|
|||||||
userUpdated(user) {
|
userUpdated(user) {
|
||||||
if (this.$store.state.user.user.id === user.id) {
|
if (this.$store.state.user.user.id === user.id) {
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
this.$store.commit('user/setSettings', user.settings)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userOnline(user) {
|
userOnline(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userOffline(user) {
|
userOffline(user) {
|
||||||
this.$store.commit('users/removeUser', user)
|
this.$store.commit('users/removeUserOnline', user)
|
||||||
},
|
},
|
||||||
userStreamUpdate(user) {
|
userStreamUpdate(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userMediaProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateMediaProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
},
|
},
|
||||||
collectionAdded(collection) {
|
collectionAdded(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||||
},
|
},
|
||||||
collectionUpdated(collection) {
|
collectionUpdated(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||||
},
|
},
|
||||||
collectionRemoved(collection) {
|
collectionRemoved(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
if (this.$route.name.startsWith('collection')) {
|
if (this.$route.name.startsWith('collection')) {
|
||||||
if (this.$route.params.id === collection.id) {
|
if (this.$route.params.id === collection.id) {
|
||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`)
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/collections`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$store.commit('libraries/removeCollection', collection)
|
this.$store.commit('libraries/removeCollection', collection)
|
||||||
},
|
},
|
||||||
|
playlistAdded(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
},
|
||||||
|
playlistUpdated(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
},
|
||||||
|
playlistRemoved(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
|
||||||
|
if (this.$route.name.startsWith('playlist')) {
|
||||||
|
if (this.$route.params.id === playlist.id) {
|
||||||
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/playlists`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||||
|
},
|
||||||
rssFeedOpen(data) {
|
rssFeedOpen(data) {
|
||||||
this.$store.commit('feeds/addFeed', data)
|
this.$store.commit('feeds/addFeed', data)
|
||||||
},
|
},
|
||||||
@@ -333,6 +351,9 @@ export default {
|
|||||||
this.$toast.info(toast)
|
this.$toast.info(toast)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
adminMessageEvt(message) {
|
||||||
|
this.$toast.info(message)
|
||||||
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
this.socket = this.$nuxtSocket({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
@@ -345,6 +366,7 @@ export default {
|
|||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
console.log('Socket initialized')
|
console.log('Socket initialized')
|
||||||
|
|
||||||
|
// Pre-defined socket events
|
||||||
this.socket.on('connect', this.connect)
|
this.socket.on('connect', this.connect)
|
||||||
this.socket.on('connect_error', this.connectError)
|
this.socket.on('connect_error', this.connectError)
|
||||||
this.socket.on('disconnect', this.disconnect)
|
this.socket.on('disconnect', this.disconnect)
|
||||||
@@ -353,6 +375,7 @@ export default {
|
|||||||
this.socket.io.on('reconnect_error', this.reconnectError)
|
this.socket.io.on('reconnect_error', this.reconnectError)
|
||||||
this.socket.io.on('reconnect_failed', this.reconnectFailed)
|
this.socket.io.on('reconnect_failed', this.reconnectFailed)
|
||||||
|
|
||||||
|
// Event received after authorizing socket
|
||||||
this.socket.on('init', this.init)
|
this.socket.on('init', this.init)
|
||||||
|
|
||||||
// Stream Listeners
|
// Stream Listeners
|
||||||
@@ -382,11 +405,16 @@ export default {
|
|||||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||||
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
||||||
|
|
||||||
// User Collection Listeners
|
// Collection Listeners
|
||||||
this.socket.on('collection_added', this.collectionAdded)
|
this.socket.on('collection_added', this.collectionAdded)
|
||||||
this.socket.on('collection_updated', this.collectionUpdated)
|
this.socket.on('collection_updated', this.collectionUpdated)
|
||||||
this.socket.on('collection_removed', this.collectionRemoved)
|
this.socket.on('collection_removed', this.collectionRemoved)
|
||||||
|
|
||||||
|
// User Playlist Listeners
|
||||||
|
this.socket.on('playlist_added', this.playlistAdded)
|
||||||
|
this.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
|
this.socket.on('playlist_removed', this.playlistRemoved)
|
||||||
|
|
||||||
// Scan Listeners
|
// Scan Listeners
|
||||||
this.socket.on('scan_start', this.scanStart)
|
this.socket.on('scan_start', this.scanStart)
|
||||||
this.socket.on('scan_complete', this.scanComplete)
|
this.socket.on('scan_complete', this.scanComplete)
|
||||||
@@ -403,6 +431,8 @@ export default {
|
|||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
|
|
||||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||||
|
|
||||||
|
this.socket.on('admin_message', this.adminMessageEvt)
|
||||||
},
|
},
|
||||||
showUpdateToast(versionData) {
|
showUpdateToast(versionData) {
|
||||||
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
||||||
@@ -472,9 +502,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Batch selecting
|
// Batch selecting
|
||||||
if (this.$store.getters['getNumLibraryItemsSelected'] && name === 'Escape') {
|
if (this.$store.getters['globals/getIsBatchSelectingMediaItems'] && name === 'Escape') {
|
||||||
// ESCAPE key cancels batch selection
|
// ESCAPE key cancels batch selection
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||||||
import LazyBookCard from '@/components/cards/LazyBookCard'
|
import LazyBookCard from '@/components/cards/LazyBookCard'
|
||||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||||
|
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -15,6 +16,7 @@ export default {
|
|||||||
getComponentClass() {
|
getComponentClass() {
|
||||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||||
|
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
async mountEntityCard(index) {
|
async mountEntityCard(index) {
|
||||||
@@ -30,7 +32,7 @@ export default {
|
|||||||
shelfEl.appendChild(bookComponent.$el)
|
shelfEl.appendChild(bookComponent.$el)
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
bookComponent.setSelectionMode(true)
|
bookComponent.setSelectionMode(true)
|
||||||
if (this.selectedLibraryItems.includes(bookComponent.libraryItemId) || this.isSelectAll) {
|
if (this.selectedMediaItems.some(i => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
|
||||||
bookComponent.selected = true
|
bookComponent.selected = true
|
||||||
} else {
|
} else {
|
||||||
bookComponent.selected = false
|
bookComponent.selected = false
|
||||||
@@ -87,7 +89,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
instance.setSelectionMode(true)
|
instance.setSelectionMode(true)
|
||||||
if (instance.libraryItemId && this.selectedLibraryItems.includes(instance.libraryItemId) || this.isSelectAll) {
|
if (instance.libraryItemId && this.selectedMediaItems.some(i => i.id === instance.libraryItemId) || this.isSelectAll) {
|
||||||
instance.selected = true
|
instance.selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,10 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
enabled: false,
|
offline: false,
|
||||||
|
cacheAssets: false,
|
||||||
|
preCaching: [],
|
||||||
|
runtimeCaching: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.5",
|
"version": "2.2.9",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.5",
|
"version": "2.2.9",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.5",
|
"version": "2.2.9",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export default {
|
|||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
|
this.$store.commit('libraries/setUserPlaylists', [])
|
||||||
|
this.$store.commit('libraries/setCollections', [])
|
||||||
this.$router.push('/login')
|
this.$router.push('/login')
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex items-center py-4 max-w-7xl mx-auto">
|
<div class="flex items-center py-4 px-2 md:px-0 max-w-7xl mx-auto">
|
||||||
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
||||||
<h1 class="text-xl">{{ title }}</h1>
|
<h1 class="text-lg lg:text-xl">{{ title }}</h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<button class="w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click="editItem">
|
<button class="w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click="editItem">
|
||||||
<span class="material-icons text-base">edit</span>
|
<span class="material-icons text-base">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<p class="text-base">{{ $strings.LabelDuration }}:</p>
|
<p class="text-base hidden md:block">{{ $strings.LabelDuration }}:</p>
|
||||||
<p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap-reverse justify-center py-4">
|
<div class="flex flex-wrap-reverse justify-center py-4 px-2">
|
||||||
<div class="w-full max-w-3xl py-4">
|
<div class="w-full max-w-3xl py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 hidden lg:block" />
|
||||||
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||||
<div class="w-40" />
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-3 py-1">
|
<div class="flex items-center mb-3 py-1">
|
||||||
<div class="flex-grow" />
|
<div class="w-12 hidden lg:block" />
|
||||||
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
||||||
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
||||||
<ui-btn color="success" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
|
<div class="flex-grow" />
|
||||||
<div class="w-40" />
|
<ui-btn v-if="hasChanges" small class="mx-2" @click.stop="resetChapters">{{ $strings.ButtonReset }}</ui-btn>
|
||||||
|
<ui-btn v-if="hasChanges" color="success" :disabled="!hasChanges" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="showShiftTimes" class="flex mb-4">
|
<div v-if="showShiftTimes" class="flex mb-4">
|
||||||
<div class="w-12"></div>
|
<div class="w-12 hidden lg:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
|
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
|
||||||
@@ -42,32 +45,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
|
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40"></div>
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="w-12"></div>
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
|
||||||
<div class="w-32 px-2">{{ $strings.LabelStart }}</div>
|
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-2">{{ $strings.LabelStart }}</div>
|
||||||
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
|
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
|
||||||
<div class="w-40"></div>
|
<div class="w-32"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="chapter in newChapters">
|
<template v-for="chapter in newChapters">
|
||||||
<div :key="chapter.id" class="flex py-1">
|
<div :key="chapter.id" class="flex py-1">
|
||||||
<div class="w-12">#{{ chapter.id + 1 }}</div>
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
|
||||||
<div class="w-32 px-1">
|
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-1">
|
||||||
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
||||||
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input v-model="chapter.title" class="text-xs" />
|
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 px-2 py-1">
|
<div class="w-32 min-w-32 px-2 py-1">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
||||||
<span class="material-icons-outlined text-base">remove</span>
|
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
||||||
</button>
|
<span class="material-icons-outlined text-base">remove</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
||||||
@@ -75,11 +80,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
||||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||||
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
||||||
</button>
|
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
||||||
@@ -92,8 +99,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-xl py-4">
|
<div class="w-full max-w-xl py-4 px-2">
|
||||||
<p class="text-lg mb-4 font-semibold py-1">{{ $strings.HeaderAudioTracks }}</p>
|
<div class="flex items-center mb-4 py-1">
|
||||||
|
<p class="text-lg font-semibold">{{ $strings.HeaderAudioTracks }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn>
|
||||||
|
<ui-tooltip :text="$strings.MessageSetChaptersFromTracksDescription" direction="top" class="flex items-center mx-1 cursor-default">
|
||||||
|
<span class="material-icons-outlined text-xl text-gray-200">info</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
||||||
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
||||||
@@ -173,8 +187,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
||||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top">
|
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
||||||
<span class="material-icons-outlined">info</span>
|
<span class="material-icons-outlined text-xl text-gray-200">info</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
||||||
@@ -186,6 +200,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, app, redirect, from }) {
|
async asyncData({ store, params, app, redirect, from }) {
|
||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
@@ -228,7 +244,8 @@ export default {
|
|||||||
showFindChaptersModal: false,
|
showFindChaptersModal: false,
|
||||||
chapterData: null,
|
chapterData: null,
|
||||||
showSecondInputs: false,
|
showSecondInputs: false,
|
||||||
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES']
|
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
|
||||||
|
hasChanges: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -270,6 +287,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setChaptersFromTracks() {
|
||||||
|
let currentStartTime = 0
|
||||||
|
let index = 0
|
||||||
|
const chapters = []
|
||||||
|
for (const track of this.audioTracks) {
|
||||||
|
chapters.push({
|
||||||
|
id: index++,
|
||||||
|
title: path.basename(track.metadata.filename, path.extname(track.metadata.filename)),
|
||||||
|
start: currentStartTime,
|
||||||
|
end: currentStartTime + track.duration
|
||||||
|
})
|
||||||
|
currentStartTime += track.duration
|
||||||
|
}
|
||||||
|
this.newChapters = chapters
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
|
},
|
||||||
shiftChapterTimes() {
|
shiftChapterTimes() {
|
||||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||||
return
|
return
|
||||||
@@ -300,7 +334,6 @@ export default {
|
|||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
addChapter(chapter) {
|
addChapter(chapter) {
|
||||||
console.log('Add chapter', chapter)
|
|
||||||
const newChapter = {
|
const newChapter = {
|
||||||
id: chapter.id + 1,
|
id: chapter.id + 1,
|
||||||
start: chapter.start,
|
start: chapter.start,
|
||||||
@@ -315,22 +348,41 @@ export default {
|
|||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
checkChapters() {
|
checkChapters() {
|
||||||
var previousStart = 0
|
let previousStart = 0
|
||||||
|
let hasChanges = this.newChapters.length !== this.chapters.length
|
||||||
|
|
||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
this.newChapters[i].id = i
|
this.newChapters[i].id = i
|
||||||
this.newChapters[i].start = Number(this.newChapters[i].start)
|
this.newChapters[i].start = Number(this.newChapters[i].start)
|
||||||
|
this.newChapters[i].title = (this.newChapters[i].title || '').trim()
|
||||||
|
|
||||||
if (i === 0 && this.newChapters[i].start !== 0) {
|
if (i === 0 && this.newChapters[i].start !== 0) {
|
||||||
this.newChapters[i].error = 'First chapter must start at 0'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorFirstNotZero
|
||||||
} else if (this.newChapters[i].start <= previousStart && i > 0) {
|
} else if (this.newChapters[i].start <= previousStart && i > 0) {
|
||||||
this.newChapters[i].error = 'Invalid start time must be >= previous chapter start time'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorStartLtPrev
|
||||||
} else if (this.newChapters[i].start >= this.mediaDuration) {
|
} else if (this.newChapters[i].start >= this.mediaDuration) {
|
||||||
this.newChapters[i].error = 'Invalid start time must be < duration'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorStartGteDuration
|
||||||
} else {
|
} else {
|
||||||
this.newChapters[i].error = null
|
this.newChapters[i].error = null
|
||||||
}
|
}
|
||||||
previousStart = this.newChapters[i].start
|
previousStart = this.newChapters[i].start
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingChapter = this.chapters[i]
|
||||||
|
if (existingChapter) {
|
||||||
|
const { start, end, title } = this.newChapters[i]
|
||||||
|
if (start !== existingChapter.start || end !== existingChapter.end || title !== existingChapter.title) {
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasChanges = hasChanges
|
||||||
},
|
},
|
||||||
playChapter(chapter) {
|
playChapter(chapter) {
|
||||||
console.log('Play Chapter', chapter.id)
|
console.log('Play Chapter', chapter.id)
|
||||||
@@ -349,8 +401,6 @@ export default {
|
|||||||
const audioTrack = this.tracks.find((at) => {
|
const audioTrack = this.tracks.find((at) => {
|
||||||
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
|
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
|
||||||
})
|
})
|
||||||
console.log('audio track', audioTrack)
|
|
||||||
|
|
||||||
this.selectedChapter = chapter
|
this.selectedChapter = chapter
|
||||||
this.isLoadingChapter = true
|
this.isLoadingChapter = true
|
||||||
|
|
||||||
@@ -365,7 +415,6 @@ export default {
|
|||||||
if (this.$isDev) {
|
if (this.$isDev) {
|
||||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||||
}
|
}
|
||||||
console.log('src', src)
|
|
||||||
|
|
||||||
audioEl.src = src
|
audioEl.src = src
|
||||||
audioEl.id = 'chapter-audio'
|
audioEl.id = 'chapter-audio'
|
||||||
@@ -409,11 +458,11 @@ export default {
|
|||||||
|
|
||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
if (this.newChapters[i].error) {
|
if (this.newChapters[i].error) {
|
||||||
this.$toast.error('Chapters have errors')
|
this.$toast.error(this.$strings.ToastChaptersHaveErrors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newChapters[i].title) {
|
if (!this.newChapters[i].title) {
|
||||||
this.$toast.error('Chapters must have titles')
|
this.$toast.error(this.$strings.ToastChaptersMustHaveTitles)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,22 +509,25 @@ export default {
|
|||||||
|
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
applyChapterData() {
|
applyChapterData() {
|
||||||
var index = 0
|
let index = 0
|
||||||
this.newChapters = this.chapterData.chapters
|
this.newChapters = this.chapterData.chapters
|
||||||
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
||||||
.map((chap) => {
|
.map((chap) => {
|
||||||
var chapEnd = Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000)
|
|
||||||
return {
|
return {
|
||||||
id: index++,
|
id: index++,
|
||||||
start: chap.startOffsetMs / 1000,
|
start: chap.startOffsetMs / 1000,
|
||||||
end: chapEnd,
|
end: Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000),
|
||||||
title: chap.title
|
title: chap.title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
findChapters() {
|
findChapters() {
|
||||||
if (!this.asinInput) {
|
if (!this.asinInput) {
|
||||||
@@ -509,22 +561,38 @@ export default {
|
|||||||
this.$toast.error('Failed to find chapters')
|
this.$toast.error('Failed to find chapters')
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
resetChapters() {
|
||||||
|
const payload = {
|
||||||
|
message: this.$strings.MessageResetChaptersConfirm,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.initChapters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
initChapters() {
|
||||||
|
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
||||||
|
if (!this.newChapters.length) {
|
||||||
|
this.newChapters = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
start: 0,
|
||||||
|
end: this.mediaDuration,
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.checkChapters()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||||
this.asinInput = this.mediaMetadata.asin || null
|
this.asinInput = this.mediaMetadata.asin || null
|
||||||
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
this.initChapters()
|
||||||
if (!this.newChapters.length) {
|
|
||||||
this.newChapters = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
start: 0,
|
|
||||||
end: this.mediaDuration,
|
|
||||||
title: ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.destroyAudioEl()
|
this.destroyAudioEl()
|
||||||
|
|||||||
@@ -2,14 +2,25 @@
|
|||||||
<div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
<div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="border border-white border-opacity-10 max-w-7xl mx-auto mb-10 mt-5">
|
<div class="border border-white border-opacity-10 max-w-7xl mx-auto mb-10 mt-5">
|
||||||
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-icons">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
<span class="material-icons text-2xl">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
||||||
|
|
||||||
<p class="ml-4 text-gray-200 text-lg">Map details</p>
|
<p class="ml-4 text-gray-200 text-lg">{{ $strings.HeaderMapDetails }}</p>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<div class="w-64 flex">
|
||||||
|
<button class="w-32 h-8 rounded-l-md shadow-md border border-gray-600" :class="!isMapOverwrite ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'overwrite'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelOverwrite }}</p>
|
||||||
|
</button>
|
||||||
|
<button class="w-32 h-8 rounded-r-md shadow-md border border-gray-600" :class="!isMapAppend ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'append'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelAppend }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,13 +29,13 @@
|
|||||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||||
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
||||||
@@ -38,15 +49,15 @@
|
|||||||
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
||||||
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<ui-checkbox
|
<ui-checkbox
|
||||||
@@ -91,14 +102,19 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, redirect, app }) {
|
async asyncData({ store, redirect, app }) {
|
||||||
if (!store.state.selectedLibraryItems.length) {
|
if (!store.state.globals.selectedMediaItems.length) {
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
var libraryItems = await app.$axios.$post(`/api/items/batch/get`, { libraryItemIds: store.state.selectedLibraryItems }).catch((error) => {
|
|
||||||
var errorMsg = error.response.data || 'Failed to get items'
|
const libraryItemIds = store.state.globals.selectedMediaItems.map((i) => i.id)
|
||||||
console.error(errorMsg, error)
|
const libraryItems = await app.$axios
|
||||||
return []
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
})
|
.then((res) => res.libraryItems)
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
|
console.error(errorMsg, error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
mediaType: libraryItems[0].mediaType,
|
mediaType: libraryItems[0].mediaType,
|
||||||
libraryItems
|
libraryItems
|
||||||
@@ -109,10 +125,10 @@ export default {
|
|||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
libraryItemCopies: [],
|
libraryItemCopies: [],
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
newSeriesNames: [],
|
|
||||||
newTagItems: [],
|
newTagItems: [],
|
||||||
newGenreItems: [],
|
newGenreItems: [],
|
||||||
newNarratorItems: [],
|
newNarratorItems: [],
|
||||||
|
mapDetailsType: 'overwrite',
|
||||||
batchDetails: {
|
batchDetails: {
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
authors: null,
|
authors: null,
|
||||||
@@ -137,10 +153,17 @@ export default {
|
|||||||
language: false,
|
language: false,
|
||||||
explicit: false
|
explicit: false
|
||||||
},
|
},
|
||||||
|
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||||
openMapOptions: false
|
openMapOptions: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMapOverwrite() {
|
||||||
|
return this.mapDetailsType === 'overwrite'
|
||||||
|
},
|
||||||
|
isMapAppend() {
|
||||||
|
return this.mapDetailsType === 'append'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
@@ -153,9 +176,6 @@ export default {
|
|||||||
tagItems() {
|
tagItems() {
|
||||||
return this.tags.concat(this.newTagItems)
|
return this.tags.concat(this.newTagItems)
|
||||||
},
|
},
|
||||||
seriesItems() {
|
|
||||||
return [...this.existingSeriesNames, ...this.newSeriesNames]
|
|
||||||
},
|
|
||||||
narratorItems() {
|
narratorItems() {
|
||||||
return [...this.narrators, ...this.newNarratorItems]
|
return [...this.narrators, ...this.newNarratorItems]
|
||||||
},
|
},
|
||||||
@@ -214,31 +234,32 @@ export default {
|
|||||||
mapBatchDetails() {
|
mapBatchDetails() {
|
||||||
this.blurBatchForm()
|
this.blurBatchForm()
|
||||||
|
|
||||||
var batchMapPayload = {}
|
const batchMapPayload = {}
|
||||||
for (const key in this.selectedBatchUsage) {
|
for (const key in this.selectedBatchUsage) {
|
||||||
if (this.selectedBatchUsage[key]) {
|
if (!this.selectedBatchUsage[key]) continue
|
||||||
if (key === 'series') {
|
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
|
||||||
// Map string of series to series objects
|
|
||||||
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
if (key === 'series') {
|
||||||
var existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
// Map string of series to series objects
|
||||||
if (existingSeries) {
|
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
||||||
return existingSeries
|
const existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
||||||
} else {
|
if (existingSeries) {
|
||||||
return {
|
return existingSeries
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
} else {
|
||||||
name: seItem
|
return {
|
||||||
}
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
name: seItem
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
} else {
|
})
|
||||||
batchMapPayload[key] = this.batchDetails[key]
|
} else {
|
||||||
}
|
batchMapPayload[key] = this.batchDetails[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.libraryItemCopies.forEach((li) => {
|
this.libraryItemCopies.forEach((li) => {
|
||||||
var ref = this.getEditFormRef(li.id)
|
const ref = this.getEditFormRef(li.id)
|
||||||
ref.mapBatchDetails(batchMapPayload)
|
ref.mapBatchDetails(batchMapPayload, this.mapDetailsType)
|
||||||
})
|
})
|
||||||
this.$toast.success('Details mapped')
|
this.$toast.success('Details mapped')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,15 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
||||||
|
<span class="material-icons text-xl">edit</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
<ui-context-menu-dropdown :items="contextMenuItems" class="mx-px" @action="contextMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl">
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
<div v-show="processing" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,6 +54,11 @@ export default {
|
|||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If collection is a different library then set library as current
|
||||||
|
if (collection.libraryId !== store.state.libraries.currentLibraryId) {
|
||||||
|
await store.dispatch('libraries/fetch', collection.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
store.commit('libraries/addUpdateCollection', collection)
|
store.commit('libraries/addUpdateCollection', collection)
|
||||||
return {
|
return {
|
||||||
collectionId: collection.id
|
collectionId: collection.id
|
||||||
@@ -59,8 +66,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingRemove: false,
|
processing: false
|
||||||
collectionCopy: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -88,7 +94,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
streaming() {
|
streaming() {
|
||||||
return !!this.playableBooks.find((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
return !!this.playableBooks.some((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return this.playableBooks.length
|
return this.playableBooks.length
|
||||||
@@ -98,26 +104,66 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.$strings.MessagePlaylistCreateFromCollection,
|
||||||
|
action: 'create-playlist'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.userCanDelete) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
this.removeClick()
|
||||||
|
} else if (action === 'create-playlist') {
|
||||||
|
this.createPlaylistFromCollection()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaylistFromCollection() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/collection/${this.collectionId}`)
|
||||||
|
.then((playlist) => {
|
||||||
|
if (playlist) {
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess)
|
||||||
|
this.$router.push(`/playlist/${playlist.id}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || this.$strings.ToastPlaylistCreateFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$store.commit('globals/setEditCollection', this.collection)
|
this.$store.commit('globals/setEditCollection', this.collection)
|
||||||
},
|
},
|
||||||
removeClick() {
|
removeClick() {
|
||||||
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) {
|
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||||
this.processingRemove = true
|
this.processing = true
|
||||||
var collectionName = this.collectionName
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/collections/${this.collection.id}`)
|
.$delete(`/api/collections/${this.collection.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.processingRemove = false
|
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
|
||||||
this.$toast.success(`Collection "${collectionName}" Removed`)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove collection', error)
|
console.error('Failed to remove collection', error)
|
||||||
this.processingRemove = false
|
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||||
this.$toast.error(`Failed to remove collection`)
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,4 +211,4 @@ export default {
|
|||||||
mounted() {},
|
mounted() {},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+15
-4
@@ -3,7 +3,7 @@
|
|||||||
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
||||||
<div class="configContent" :class="`page-${currentPage}`">
|
<div class="configContent" :class="`page-${currentPage}`">
|
||||||
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
||||||
<span class="material-icons cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
<span class="material-icons text-2xl cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
||||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
@@ -42,10 +42,21 @@ export default {
|
|||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
currentPage() {
|
currentPage() {
|
||||||
if (!this.$route.name) return 'Settings'
|
if (!this.$route.name) return this.$strings.HeaderSettings
|
||||||
var routeName = this.$route.name.split('-')
|
var routeName = this.$route.name.split('-')
|
||||||
if (routeName.length > 0) return routeName.slice(1).join('-')
|
if (routeName.length > 0) {
|
||||||
return 'Settings'
|
const pageName = routeName.slice(1).join('-')
|
||||||
|
if (pageName === 'log') return this.$strings.HeaderLogs
|
||||||
|
else if (pageName === 'backups') return this.$strings.HeaderBackups
|
||||||
|
else if (pageName === 'libraries') return this.$strings.HeaderLibraries
|
||||||
|
else if (pageName === 'notifications') return this.$strings.HeaderNotifications
|
||||||
|
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
||||||
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
|
}
|
||||||
|
return this.$strings.HeaderSettings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderBackups }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-base mb-2 text-gray-300">{{ $strings.MessageBackupsDescription }} <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>.</p>
|
|
||||||
<p class="text-base mb-4 text-gray-300">{{ $strings.MessageBackupsNote }}</p>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
||||||
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
|
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
|
||||||
@@ -17,7 +10,7 @@
|
|||||||
|
|
||||||
<div v-if="enableBackups" class="mb-6">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<div class="flex items-center pl-6">
|
<div class="flex items-center pl-6">
|
||||||
<span class="material-icons-outlined text-black-50">schedule</span>
|
<span class="material-icons-outlined text-2xl text-black-50">schedule</span>
|
||||||
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
||||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,9 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
|
<app-settings-content :header-text="$strings.HeaderSettings">
|
||||||
<div class="mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderSettings }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:flex">
|
<div class="lg:flex">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
@@ -15,7 +11,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +21,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +31,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +53,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +63,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +89,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsParseSubtitles }}
|
{{ $strings.LabelSettingsParseSubtitles }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +99,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsFindCovers }}
|
{{ $strings.LabelSettingsFindCovers }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -117,7 +113,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +123,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +133,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +143,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +153,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsDisableWatcher }}
|
{{ $strings.LabelSettingsDisableWatcher }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +168,7 @@
|
|||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsExperimentalFeatures }}
|
{{ $strings.LabelSettingsExperimentalFeatures }}
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -183,7 +179,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsEnableEReader }}
|
{{ $strings.LabelSettingsEnableEReader }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,22 +189,23 @@
|
|||||||
<ui-tooltip text="Tone library for metadata">
|
<ui-tooltip text="Tone library for metadata">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
Use Tone library for metadata
|
Use Tone library for metadata
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeCache">{{ $strings.ButtonPurgeAllCache }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isPurgingCache" @click.stop="purgeCache">{{ $strings.ButtonPurgeAllCache }}</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isPurgingCache" @click.stop="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">{{ $strings.ButtonRemoveAllLibraryItems }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isResettingLibraryItems" @click="resetLibraryItems">{{ $strings.ButtonRemoveAllLibraryItems }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm font-book text-yellow-400">
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageGenres }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!genres.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoGenres }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(genre, index) in genres">
|
||||||
|
<div :key="genre" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingGenre !== genre" class="text-sm md:text-base text-gray-100">{{ genre }}</p>
|
||||||
|
<ui-text-input v-else v-model="newGenreName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingGenre !== genre">
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editClick(genre)" />
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeClick(genre)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
genres: [],
|
||||||
|
editingGenre: null,
|
||||||
|
newGenreName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newGenreName = ''
|
||||||
|
this.editingGenre = null
|
||||||
|
},
|
||||||
|
removeClick(genre) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove genre "${genre}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeGenre(genre)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
editClick(genre) {
|
||||||
|
this.newGenreName = genre
|
||||||
|
this.editingGenre = genre
|
||||||
|
},
|
||||||
|
saveClick() {
|
||||||
|
this.newGenreName = this.newGenreName.trim()
|
||||||
|
if (!this.newGenreName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingGenre === this.newGenreName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const genreNameExists = this.genres.find((g) => g !== this.editingGenre && g === this.newGenreName)
|
||||||
|
const genreNameExistsOfDifferentCase = !genreNameExists ? this.genres.find((g) => g !== this.editingGenre && g.toLowerCase() === this.newGenreName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName])
|
||||||
|
if (genreNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameGenreMergeNote}</span>`
|
||||||
|
} else if (genreNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameGenre()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameGenre() {
|
||||||
|
this.loading = true
|
||||||
|
let _newGenreName = this.newGenreName
|
||||||
|
let _editingGenre = this.editingGenre
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
genre: _editingGenre,
|
||||||
|
newGenre: _newGenreName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/genres/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.genreMerged) {
|
||||||
|
this.genres = this.genres.filter((g) => g !== _newGenreName)
|
||||||
|
}
|
||||||
|
this.genres = this.genres.map((g) => {
|
||||||
|
if (g === _editingGenre) return _newGenreName
|
||||||
|
return g
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename genre', error)
|
||||||
|
this.$toast.error('Failed to rename genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeGenre(genre) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/genres/${this.$encode(genre)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.genres = this.genres.filter((g) => g !== genre)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove genre', error)
|
||||||
|
this.$toast.error('Failed to remove genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/genres')
|
||||||
|
.then((data) => {
|
||||||
|
this.genres = (data.genres || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load genres', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="'Item Metadata Utils'">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/tags" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 mt-6 mb-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageTags }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/genres" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageGenres }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</app-settings-content>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
init() {}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageTags }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!tags.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoTags }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(tag, index) in tags">
|
||||||
|
<div :key="tag" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingTag !== tag" class="text-sm md:text-base text-gray-100">{{ tag }}</p>
|
||||||
|
<ui-text-input v-else v-model="newTagName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingTag !== tag">
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
tags: [],
|
||||||
|
editingTag: null,
|
||||||
|
newTagName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newTagName = ''
|
||||||
|
this.editingTag = null
|
||||||
|
},
|
||||||
|
removeTagClick(tag) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove tag "${tag}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeTag(tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
saveTagClick() {
|
||||||
|
this.newTagName = this.newTagName.trim()
|
||||||
|
if (!this.newTagName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingTag === this.newTagName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagNameExists = this.tags.find((t) => t !== this.editingTag && t === this.newTagName)
|
||||||
|
const tagNameExistsOfDifferentCase = !tagNameExists ? this.tags.find((t) => t !== this.editingTag && t.toLowerCase() === this.newTagName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName])
|
||||||
|
if (tagNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameTagMergeNote}</span>`
|
||||||
|
} else if (tagNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameTag()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameTag() {
|
||||||
|
this.loading = true
|
||||||
|
let _newTagName = this.newTagName
|
||||||
|
let _editingTag = this.editingTag
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
tag: _editingTag,
|
||||||
|
newTag: _newTagName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/tags/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.tagMerged) {
|
||||||
|
this.tags = this.tags.filter((t) => t !== _newTagName)
|
||||||
|
}
|
||||||
|
this.tags = this.tags.map((t) => {
|
||||||
|
if (t === _editingTag) return _newTagName
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename tag', error)
|
||||||
|
this.$toast.error('Failed to rename tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeTag(tag) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/tags/${this.$encode(tag)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.tags = this.tags.filter((t) => t !== tag)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove tag', error)
|
||||||
|
this.$toast.error('Failed to remove tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editTagClick(tag) {
|
||||||
|
this.newTagName = tag
|
||||||
|
this.editingTag = tag
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/tags')
|
||||||
|
.then((data) => {
|
||||||
|
this.tags = (data.tags || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load tags', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
<app-settings-content :header-text="$strings.HeaderLibraries" show-add-button @clicked="setShowLibraryModal">
|
||||||
|
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
||||||
|
</app-settings-content>
|
||||||
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,66 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLibraryStats }}: {{ currentLibraryName }}</h1>
|
<app-settings-content :header-text="$strings.HeaderLibraryStats + ': ' + currentLibraryName">
|
||||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||||
|
|
||||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||||
<template v-for="genre in top5Genres">
|
<template v-for="genre in top5Genres">
|
||||||
<div :key="genre.genre" class="w-full py-2">
|
<div :key="genre.genre" class="w-full py-2">
|
||||||
<div class="flex items-end mb-1">
|
<div class="flex items-end mb-1">
|
||||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
||||||
{{ genre.genre }}
|
{{ genre.genre }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
</template>
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||||
|
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||||
|
<template v-for="(author, index) in top10Authors">
|
||||||
|
<div :key="author.id" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ author.count }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||||
|
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
|
<template v-for="(ab, index) in top10LongestItems">
|
||||||
|
<div :key="index" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
</app-settings-content>
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
|
||||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
|
||||||
<template v-for="(author, index) in top10Authors">
|
|
||||||
<div :key="author.id" class="w-full py-2">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
|
||||||
</p>
|
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
|
||||||
</div>
|
|
||||||
<div class="w-4 ml-3">
|
|
||||||
<p class="text-sm font-bold">{{ author.count }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="w-80 my-6 mx-auto">
|
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
|
||||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
|
||||||
<template v-for="(ab, index) in top10LongestItems">
|
|
||||||
<div :key="index" class="w-full py-2">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
|
||||||
</p>
|
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
|
||||||
</div>
|
|
||||||
<div class="w-4 ml-3">
|
|
||||||
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+19
-20
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderLogs">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLogs }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between mb-2 place-items-end">
|
<div class="flex justify-between mb-2 place-items-end">
|
||||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@
|
|||||||
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
|
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,20 +35,6 @@ export default {
|
|||||||
searchText: null,
|
searchText: null,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
|
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
|
||||||
logLevels: [
|
|
||||||
{
|
|
||||||
value: 1,
|
|
||||||
text: 'Debug'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
text: 'Info'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 3,
|
|
||||||
text: 'Warn'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
loadedLogs: []
|
loadedLogs: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -66,6 +49,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
logLevels() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
text: this.$strings.LabelLogLevelDebug
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
text: this.$strings.LabelLogLevelInfo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
text: this.$strings.LabelLogLevelWarn
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
logLevelItems() {
|
logLevelItems() {
|
||||||
if (process.env.NODE_ENV === 'production') return this.logLevels
|
if (process.env.NODE_ENV === 'production') return this.logLevels
|
||||||
this.logLevels.unshift({ text: 'Trace', value: 0 })
|
this.logLevels.unshift({ text: 'Trace', value: 0 })
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto">
|
<app-settings-content :header-text="$strings.HeaderAppriseNotificationSettings" :description="$strings.MessageAppriseDescription">
|
||||||
<h2 class="text-xl font-semibold mb-4">{{ $strings.HeaderAppriseNotificationSettings }}</h2>
|
|
||||||
<p id="appriseDescription" class="mb-6 text-gray-200" v-html="$strings.MessageAppriseDescription" />
|
|
||||||
|
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<ui-text-input-with-label ref="apiUrlInput" v-model="appriseApiUrl" :disabled="savingSettings" label="Apprise API Url" class="mb-2" />
|
<ui-text-input-with-label ref="apiUrlInput" v-model="appriseApiUrl" :disabled="savingSettings" label="Apprise API Url" class="mb-2" />
|
||||||
|
|
||||||
@@ -41,7 +38,7 @@
|
|||||||
<template v-for="notification in notifications">
|
<template v-for="notification in notifications">
|
||||||
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
|
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" @update="updateSettings" />
|
<modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" @update="updateSettings" />
|
||||||
</div>
|
</div>
|
||||||
@@ -170,21 +167,4 @@ export default {
|
|||||||
this.$root.socket.off('notifications_updated', this.notificationsUpdated)
|
this.$root.socket.off('notifications_updated', this.notificationsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#appriseDescription a {
|
|
||||||
color: rgb(96 165 250);
|
|
||||||
}
|
|
||||||
#appriseDescription a:hover {
|
|
||||||
color: rgb(147 197 253);
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
#appriseDescription code {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: rgb(82, 82, 82);
|
|
||||||
color: white;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderListeningSessions">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderListeningSessions }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mb-2">
|
<div class="flex justify-end mb-2">
|
||||||
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
|
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
|
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
||||||
</div>
|
</div>
|
||||||
@@ -65,10 +61,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ params, redirect, app }) {
|
||||||
var users = await app.$axios
|
const users = await app.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
return users.sort((a, b) => {
|
return res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -102,7 +98,7 @@ export default {
|
|||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
},
|
},
|
||||||
userItems() {
|
userItems() {
|
||||||
var userItems = [{ value: '', text: 'All Users' }]
|
var userItems = [{ value: '', text: this.$strings.LabelAllUsers }]
|
||||||
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||||
},
|
},
|
||||||
filteredUserUsername() {
|
filteredUserUsername() {
|
||||||
|
|||||||
@@ -1,69 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<h1 class="text-xl">{{ $strings.HeaderYourStats }}</h1>
|
<app-settings-content :header-text="$strings.HeaderYourStats">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex p-2">
|
||||||
|
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="px-3">
|
||||||
|
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||||
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex p-2">
|
||||||
<div class="flex p-2">
|
<div class="hidden sm:block">
|
||||||
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
<span class="hidden sm:block material-icons-outlined text-5xl lg:text-6xl">event</span>
|
||||||
<path
|
</div>
|
||||||
fill="currentColor"
|
<div class="px-1">
|
||||||
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
|
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||||
/>
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
||||||
</svg>
|
</div>
|
||||||
<div class="px-3">
|
</div>
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
<div class="flex p-2">
|
||||||
|
<div class="hidden sm:block">
|
||||||
|
<span class="material-icons-outlined text-5xl lg:text-6xl">watch_later</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-1">
|
||||||
|
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||||
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
|
||||||
<div class="flex p-2">
|
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
||||||
<div class="hidden sm:block">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<span class="hidden sm:block material-icons-outlined text-5xl lg:text-6xl">event</span>
|
<div class="flex mb-4 items-center">
|
||||||
</div>
|
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||||
<div class="px-1">
|
<div class="flex-grow" />
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
</div>
|
||||||
</div>
|
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
||||||
</div>
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex p-2">
|
<div class="flex items-center mb-1">
|
||||||
<div class="hidden sm:block">
|
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||||
<span class="material-icons-outlined text-5xl lg:text-6xl">watch_later</span>
|
<div class="w-56">
|
||||||
</div>
|
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<div class="px-1">
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
</div>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
<div class="flex-grow" />
|
||||||
</div>
|
<div class="w-18 text-right">
|
||||||
</div>
|
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
|
|
||||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
|
||||||
<div class="w-80 my-6 mx-auto">
|
|
||||||
<div class="flex mb-4 items-center">
|
|
||||||
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
|
||||||
</div>
|
|
||||||
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
|
||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
|
||||||
<div class="w-56">
|
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<div class="w-18 text-right">
|
|
||||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
||||||
|
</app-settings-content>
|
||||||
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<tables-users-table />
|
<app-settings-content :header-text="$strings.HeaderUsers" show-add-button @clicked="setShowUserModal">
|
||||||
|
<tables-users-table />
|
||||||
|
</app-settings-content>
|
||||||
|
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
selectedAccount: null,
|
||||||
|
showAccountModal: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {},
|
methods: {
|
||||||
|
setShowUserModal(selectedAccount) {
|
||||||
|
this.selectedAccount = selectedAccount
|
||||||
|
this.showAccountModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -129,20 +129,20 @@
|
|||||||
<!-- Icon buttons -->
|
<!-- Icon buttons -->
|
||||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
||||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
|
||||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
|
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
|
||||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_add'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" :bg-color="isQueued ? 'primary' : 'success bg-opacity-60'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||||
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
<span class="material-icons text-2xl -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
{{ $strings.ButtonRead }}
|
{{ $strings.ButtonRead }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
@@ -158,6 +158,10 @@
|
|||||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="!isPodcast && tracks.length" :text="$strings.LabelYourPlaylists" direction="top">
|
||||||
|
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- Only admin or root user can download new episodes -->
|
<!-- Only admin or root user can download new episodes -->
|
||||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||||
@@ -608,6 +612,10 @@ export default {
|
|||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||||
},
|
},
|
||||||
|
playlistsClick() {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem }])
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
clickRSSFeed() {
|
clickRSSFeed() {
|
||||||
this.showRssFeedModal = true
|
this.showRssFeedModal = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,10 +48,13 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
this.authors = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors`).catch((error) => {
|
this.authors = await this.$axios
|
||||||
console.error('Failed to load authors', error)
|
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
|
||||||
return []
|
.then((response) => response.authors)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to load authors', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
authorAdded(author) {
|
authorAdded(author) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, query, store, app, redirect }) {
|
async asyncData({ params, query, store, redirect }) {
|
||||||
var libraryId = params.library
|
var libraryId = params.library
|
||||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!libraryData) {
|
if (!libraryData) {
|
||||||
@@ -15,18 +15,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set series sort by
|
// Set series sort by
|
||||||
if (params.id === 'series') {
|
if (query.filter || query.sort || query.desc) {
|
||||||
console.log('Series page', query)
|
const isSeries = params.id === 'series'
|
||||||
if (query.sort) {
|
const settingsUpdate = {
|
||||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
[isSeries ? 'seriesFilterBy' : 'filterBy']: query.filter || undefined,
|
||||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
[isSeries ? 'seriesSortBy' : 'orderBy']: query.sort || undefined,
|
||||||
|
[isSeries ? 'seriesSortDesc' : 'orderDesc']: query.desc == '0' ? false : query.desc == '1' ? true : undefined
|
||||||
}
|
}
|
||||||
if (query.filter) {
|
store.dispatch('user/updateUserSettings', settingsUpdate)
|
||||||
console.log('has filter', query.filter)
|
|
||||||
store.commit('libraries/setSeriesFilterBy', query.filter)
|
|
||||||
}
|
|
||||||
} else if (query.filter) {
|
|
||||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect podcast libraries
|
// Redirect podcast libraries
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||||
<span v-if="episodeIdStreaming === episode.id" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span v-if="episodeIdStreaming === episode.id" class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
<span v-else class="material-icons text-success">play_arrow</span>
|
<span v-else class="material-icons text-2xl text-success">play_arrow</span>
|
||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
||||||
<span class="material-icons-outlined">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
|
|
||||||
|
this.$store.dispatch('user/loadUserSettings')
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||||
|
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||||
|
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 200px">
|
||||||
|
<div class="relative" style="height: fit-content">
|
||||||
|
<covers-playlist-cover :items="playlistItems" :width="200" :height="200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
|
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
|
||||||
|
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
|
||||||
|
{{ playlistName }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||||
|
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
|
||||||
|
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-8 max-w-2xl">
|
||||||
|
<p class="text-base text-gray-100">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlistId" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, app, redirect, route }) {
|
||||||
|
if (!store.state.user.user) {
|
||||||
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
|
}
|
||||||
|
var playlist = await app.$axios.$get(`/api/playlists/${params.id}`).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!playlist) {
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If playlist is a different library then set library as current
|
||||||
|
if (playlist.libraryId !== store.state.libraries.currentLibraryId) {
|
||||||
|
await store.dispatch('libraries/fetch', playlist.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
return {
|
||||||
|
playlistId: playlist.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processingRemove: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
playlistItems() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
},
|
||||||
|
playlistName() {
|
||||||
|
return this.playlist.name || ''
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.playlist.description || ''
|
||||||
|
},
|
||||||
|
playlist() {
|
||||||
|
return this.$store.getters['libraries/getPlaylist'](this.playlistId) || {}
|
||||||
|
},
|
||||||
|
playableItems() {
|
||||||
|
return this.playlistItems.filter((item) => {
|
||||||
|
const libraryItem = item.libraryItem
|
||||||
|
if (libraryItem.isMissing || libraryItem.isInvalid) return false
|
||||||
|
if (item.episode) return item.episode.audioFile
|
||||||
|
return libraryItem.media.tracks.length
|
||||||
|
})
|
||||||
|
},
|
||||||
|
streaming() {
|
||||||
|
return !!this.playableItems.find((i) => this.$store.getters['getIsMediaStreaming'](i.libraryItemId, i.episodeId))
|
||||||
|
},
|
||||||
|
showPlayButton() {
|
||||||
|
return this.playableItems.length
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClick() {
|
||||||
|
this.$store.commit('globals/setEditPlaylist', this.playlist)
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
if (confirm(`Are you sure you want to remove playlist "${this.playlistName}"?`)) {
|
||||||
|
this.processingRemove = true
|
||||||
|
var playlistName = this.playlistName
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.processingRemove = false
|
||||||
|
this.$toast.success(`Playlist "${playlistName}" Removed`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove playlist', error)
|
||||||
|
this.processingRemove = false
|
||||||
|
this.$toast.error(`Failed to remove playlist`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickPlay() {
|
||||||
|
const queueItems = []
|
||||||
|
|
||||||
|
// Playlist queue will start at the first unfinished item
|
||||||
|
// if all items are finished then entire playlist is queued
|
||||||
|
const itemsWithProgress = this.playableItems.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
progress: this.$store.getters['user/getUserMediaProgress'](item.libraryItemId, item.episodeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished)
|
||||||
|
if (!hasUnfinishedItems) {
|
||||||
|
console.warn('All items in playlist are finished - starting at first item')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < itemsWithProgress.length; i++) {
|
||||||
|
const playlistItem = itemsWithProgress[i]
|
||||||
|
if (!hasUnfinishedItems || !playlistItem.progress || !playlistItem.progress.isFinished) {
|
||||||
|
const libraryItem = playlistItem.libraryItem
|
||||||
|
if (playlistItem.episode) {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: libraryItem.libraryId,
|
||||||
|
episodeId: playlistItem.episode.id,
|
||||||
|
title: playlistItem.episode.title,
|
||||||
|
subtitle: libraryItem.media.metadata.title,
|
||||||
|
caption: '',
|
||||||
|
duration: playlistItem.episode.duration || null,
|
||||||
|
coverPath: libraryItem.media.coverPath || null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: libraryItem.libraryId,
|
||||||
|
episodeId: null,
|
||||||
|
title: libraryItem.media.metadata.title,
|
||||||
|
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
|
||||||
|
caption: '',
|
||||||
|
duration: libraryItem.media.duration || null,
|
||||||
|
coverPath: libraryItem.media.coverPath || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueItems.length >= 0) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: queueItems[0].libraryItemId,
|
||||||
|
episodeId: queueItems[0].episodeId,
|
||||||
|
queueItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,8 +5,6 @@ import { formatDistance, format, addDays, isDate } from 'date-fns'
|
|||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
Vue.prototype.$eventBus = new Vue()
|
|
||||||
|
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
@@ -30,23 +28,26 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => {
|
|||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
||||||
if (typeof input !== 'string') {
|
if (typeof filename !== 'string') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
// Most file systems use number of bytes for max filename
|
||||||
const MAX_FILENAME_LEN = 240
|
// to support most filesystems we will use max of 255 bytes in utf-16
|
||||||
|
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
|
||||||
|
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
|
||||||
|
const MAX_FILENAME_BYTES = 255
|
||||||
|
|
||||||
var replacement = ''
|
const replacement = ''
|
||||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
const illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
const controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||||
var reservedRe = /^\.+$/
|
const reservedRe = /^\.+$/
|
||||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||||
var windowsTrailingRe = /[\. ]+$/
|
const windowsTrailingRe = /[\. ]+$/
|
||||||
var lineBreaks = /[\n\r]/g
|
const lineBreaks = /[\n\r]/g
|
||||||
|
|
||||||
var sanitized = input
|
sanitized = filename
|
||||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||||
.replace(illegalRe, replacement)
|
.replace(illegalRe, replacement)
|
||||||
.replace(controlRe, replacement)
|
.replace(controlRe, replacement)
|
||||||
@@ -55,13 +56,25 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
|||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
|
||||||
|
// Check if basename is too many bytes
|
||||||
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
|
const basename = Path.basename(sanitized, ext)
|
||||||
|
const extByteLength = Buffer.byteLength(ext, 'utf16le')
|
||||||
|
const basenameByteLength = Buffer.byteLength(basename, 'utf16le')
|
||||||
|
if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) {
|
||||||
|
const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength
|
||||||
|
let totalBytes = 0
|
||||||
|
let trimmedBasename = ''
|
||||||
|
|
||||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
// Add chars until max bytes is reached
|
||||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
for (const char of basename) {
|
||||||
var ext = Path.extname(sanitized)
|
totalBytes += Buffer.byteLength(char, 'utf16le')
|
||||||
var basename = Path.basename(sanitized, ext)
|
if (totalBytes > MaxBytesForBasename) break
|
||||||
basename = basename.slice(0, basename.length - lenToRemove)
|
else trimmedBasename += char
|
||||||
sanitized = basename + ext
|
}
|
||||||
|
|
||||||
|
trimmedBasename = trimmedBasename.trim()
|
||||||
|
sanitized = trimmedBasename + ext
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
@@ -94,13 +107,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
|
|
||||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(str).then(() => {
|
navigator.clipboard.writeText(str).then(() => {
|
||||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||||
resolve(true)
|
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('Clipboard copy failed', str, err)
|
console.error('Clipboard copy failed', str, err)
|
||||||
resolve(false)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
@@ -146,6 +157,7 @@ export {
|
|||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
app.$decode = decode
|
app.$decode = decode
|
||||||
app.$encode = encode
|
app.$encode = encode
|
||||||
|
inject('eventBus', new Vue())
|
||||||
inject('isDev', process.env.NODE_ENV !== 'production')
|
inject('isDev', process.env.NODE_ENV !== 'production')
|
||||||
|
|
||||||
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
||||||
|
|||||||
+41
-1
@@ -4,16 +4,21 @@ export const state = () => ({
|
|||||||
showBatchCollectionModal: false,
|
showBatchCollectionModal: false,
|
||||||
showCollectionsModal: false,
|
showCollectionsModal: false,
|
||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
|
showPlaylistsModal: false,
|
||||||
|
showEditPlaylistModal: false,
|
||||||
showEditPodcastEpisode: false,
|
showEditPodcastEpisode: false,
|
||||||
showViewPodcastEpisodeModal: false,
|
showViewPodcastEpisodeModal: false,
|
||||||
showConfirmPrompt: false,
|
showConfirmPrompt: false,
|
||||||
confirmPromptOptions: null,
|
confirmPromptOptions: null,
|
||||||
showEditAuthorModal: false,
|
showEditAuthorModal: false,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
|
selectedPlaylistItems: null,
|
||||||
|
selectedPlaylist: null,
|
||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
selectedAuthor: null,
|
selectedAuthor: null,
|
||||||
|
selectedMediaItems: [],
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
isChromecastInitialized: false, // Script loaded
|
isChromecastInitialized: false, // Script loadeds
|
||||||
showBatchQuickMatchModal: false,
|
showBatchQuickMatchModal: false,
|
||||||
dateFormats: [
|
dateFormats: [
|
||||||
{
|
{
|
||||||
@@ -60,6 +65,9 @@ export const getters = {
|
|||||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||||
}
|
}
|
||||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||||
|
},
|
||||||
|
getIsBatchSelectingMediaItems: (state) => {
|
||||||
|
return state.selectedMediaItems.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +87,12 @@ export const mutations = {
|
|||||||
setShowEditCollectionModal(state, val) {
|
setShowEditCollectionModal(state, val) {
|
||||||
state.showEditCollectionModal = val
|
state.showEditCollectionModal = val
|
||||||
},
|
},
|
||||||
|
setShowPlaylistsModal(state, val) {
|
||||||
|
state.showPlaylistsModal = val
|
||||||
|
},
|
||||||
|
setShowEditPlaylistModal(state, val) {
|
||||||
|
state.showEditPlaylistModal = val
|
||||||
|
},
|
||||||
setShowEditPodcastEpisodeModal(state, val) {
|
setShowEditPodcastEpisodeModal(state, val) {
|
||||||
state.showEditPodcastEpisode = val
|
state.showEditPodcastEpisode = val
|
||||||
},
|
},
|
||||||
@@ -96,9 +110,16 @@ export const mutations = {
|
|||||||
state.selectedCollection = collection
|
state.selectedCollection = collection
|
||||||
state.showEditCollectionModal = true
|
state.showEditCollectionModal = true
|
||||||
},
|
},
|
||||||
|
setEditPlaylist(state, playlist) {
|
||||||
|
state.selectedPlaylist = playlist
|
||||||
|
state.showEditPlaylistModal = true
|
||||||
|
},
|
||||||
setSelectedEpisode(state, episode) {
|
setSelectedEpisode(state, episode) {
|
||||||
state.selectedEpisode = episode
|
state.selectedEpisode = episode
|
||||||
},
|
},
|
||||||
|
setSelectedPlaylistItems(state, items) {
|
||||||
|
state.selectedPlaylistItems = items
|
||||||
|
},
|
||||||
showEditAuthorModal(state, author) {
|
showEditAuthorModal(state, author) {
|
||||||
state.selectedAuthor = author
|
state.selectedAuthor = author
|
||||||
state.showEditAuthorModal = true
|
state.showEditAuthorModal = true
|
||||||
@@ -117,5 +138,24 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
setShowBatchQuickMatchModal(state, val) {
|
setShowBatchQuickMatchModal(state, val) {
|
||||||
state.showBatchQuickMatchModal = val
|
state.showBatchQuickMatchModal = val
|
||||||
|
},
|
||||||
|
resetSelectedMediaItems(state) {
|
||||||
|
state.selectedMediaItems = []
|
||||||
|
},
|
||||||
|
toggleMediaItemSelected(state, item) {
|
||||||
|
if (state.selectedMediaItems.some(i => i.id === item.id)) {
|
||||||
|
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
||||||
|
} else {
|
||||||
|
state.selectedMediaItems.push(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setMediaItemSelected(state, { item, selected }) {
|
||||||
|
const isAlreadySelected = state.selectedMediaItems.some(i => i.id === item.id)
|
||||||
|
if (isAlreadySelected && !selected) {
|
||||||
|
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
||||||
|
|
||||||
|
} else if (selected && !isAlreadySelected) {
|
||||||
|
state.selectedMediaItems.push(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,9 @@ export const state = () => ({
|
|||||||
showEReader: false,
|
showEReader: false,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
selectedLibraryItems: [],
|
|
||||||
processingBatch: false,
|
processingBatch: false,
|
||||||
previousPath: '/',
|
previousPath: '/',
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
backups: [],
|
|
||||||
bookshelfBookIds: [],
|
bookshelfBookIds: [],
|
||||||
openModal: null,
|
openModal: null,
|
||||||
innerModalOpen: false,
|
innerModalOpen: false,
|
||||||
@@ -30,14 +28,10 @@ export const state = () => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getIsLibraryItemSelected: state => libraryItemId => {
|
|
||||||
return !!state.selectedLibraryItems.includes(libraryItemId)
|
|
||||||
},
|
|
||||||
getServerSetting: state => key => {
|
getServerSetting: state => key => {
|
||||||
if (!state.serverSettings) return null
|
if (!state.serverSettings) return null
|
||||||
return state.serverSettings[key]
|
return state.serverSettings[key]
|
||||||
},
|
},
|
||||||
getNumLibraryItemsSelected: state => state.selectedLibraryItems.length,
|
|
||||||
getLibraryItemIdStreaming: state => {
|
getLibraryItemIdStreaming: state => {
|
||||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
||||||
},
|
},
|
||||||
@@ -218,26 +212,6 @@ export const mutations = {
|
|||||||
setSelectedLibraryItem(state, val) {
|
setSelectedLibraryItem(state, val) {
|
||||||
Vue.set(state, 'selectedLibraryItem', val)
|
Vue.set(state, 'selectedLibraryItem', val)
|
||||||
},
|
},
|
||||||
setSelectedLibraryItems(state, items) {
|
|
||||||
Vue.set(state, 'selectedLibraryItems', items)
|
|
||||||
},
|
|
||||||
toggleLibraryItemSelected(state, itemId) {
|
|
||||||
if (state.selectedLibraryItems.includes(itemId)) {
|
|
||||||
state.selectedLibraryItems = state.selectedLibraryItems.filter(a => a !== itemId)
|
|
||||||
} else {
|
|
||||||
var newSel = state.selectedLibraryItems.concat([itemId])
|
|
||||||
Vue.set(state, 'selectedLibraryItems', newSel)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setLibraryItemSelected(state, { libraryItemId, selected }) {
|
|
||||||
var isThere = state.selectedLibraryItems.includes(libraryItemId)
|
|
||||||
if (isThere && !selected) {
|
|
||||||
state.selectedLibraryItems = state.selectedLibraryItems.filter(a => a !== libraryItemId)
|
|
||||||
} else if (selected && !isThere) {
|
|
||||||
var newSel = state.selectedLibraryItems.concat([libraryItemId])
|
|
||||||
Vue.set(state, 'selectedLibraryItems', newSel)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setProcessingBatch(state, val) {
|
setProcessingBatch(state, val) {
|
||||||
state.processingBatch = val
|
state.processingBatch = val
|
||||||
},
|
},
|
||||||
@@ -245,9 +219,6 @@ export const mutations = {
|
|||||||
state.showExperimentalFeatures = val
|
state.showExperimentalFeatures = val
|
||||||
localStorage.setItem('experimental', val ? 1 : 0)
|
localStorage.setItem('experimental', val ? 1 : 0)
|
||||||
},
|
},
|
||||||
setBackups(state, val) {
|
|
||||||
state.backups = val.sort((a, b) => b.createdAt - a.createdAt)
|
|
||||||
},
|
|
||||||
setOpenModal(state, val) {
|
setOpenModal(state, val) {
|
||||||
state.openModal = val
|
state.openModal = val
|
||||||
},
|
},
|
||||||
|
|||||||
+39
-20
@@ -9,10 +9,9 @@ export const state = () => ({
|
|||||||
issues: 0,
|
issues: 0,
|
||||||
folderLastUpdate: 0,
|
folderLastUpdate: 0,
|
||||||
filterData: null,
|
filterData: null,
|
||||||
seriesSortBy: 'name',
|
numUserPlaylists: 0,
|
||||||
seriesSortDesc: false,
|
collections: [],
|
||||||
seriesFilterBy: 'all',
|
userPlaylists: []
|
||||||
collections: []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -59,6 +58,9 @@ export const getters = {
|
|||||||
},
|
},
|
||||||
getCollection: state => id => {
|
getCollection: state => id => {
|
||||||
return state.collections.find(c => c.id === id)
|
return state.collections.find(c => c.id === id)
|
||||||
|
},
|
||||||
|
getPlaylist: state => id => {
|
||||||
|
return state.userPlaylists.find(p => p.id === id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +83,8 @@ export const actions = {
|
|||||||
.$get('/api/filesystem')
|
.$get('/api/filesystem')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log('Settings folders', res)
|
console.log('Settings folders', res)
|
||||||
commit('setFolders', res)
|
commit('setFolders', res.directories)
|
||||||
return res
|
return res.directories
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load dirs', error)
|
console.error('Failed to load dirs', error)
|
||||||
@@ -102,20 +104,26 @@ export const actions = {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const libraryChanging = state.currentLibraryId !== libraryId
|
||||||
return this.$axios
|
return this.$axios
|
||||||
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
var library = data.library
|
const library = data.library
|
||||||
var filterData = data.filterdata
|
const filterData = data.filterdata
|
||||||
var issues = data.issues || 0
|
const issues = data.issues || 0
|
||||||
|
const numUserPlaylists = data.numUserPlaylists
|
||||||
|
|
||||||
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
||||||
|
|
||||||
commit('addUpdate', library)
|
commit('addUpdate', library)
|
||||||
commit('setLibraryIssues', issues)
|
commit('setLibraryIssues', issues)
|
||||||
commit('setLibraryFilterData', filterData)
|
commit('setLibraryFilterData', filterData)
|
||||||
|
commit('setNumUserPlaylists', numUserPlaylists)
|
||||||
commit('setCurrentLibrary', libraryId)
|
commit('setCurrentLibrary', libraryId)
|
||||||
commit('setCollections', [])
|
if (libraryChanging) {
|
||||||
|
commit('setCollections', [])
|
||||||
|
commit('setUserPlaylists', [])
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -140,7 +148,7 @@ export const actions = {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/libraries`)
|
.$get(`/api/libraries`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
commit('set', data)
|
commit('set', data.libraries)
|
||||||
commit('setLastLoad')
|
commit('setLastLoad')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -219,6 +227,9 @@ export const mutations = {
|
|||||||
setLibraryFilterData(state, filterData) {
|
setLibraryFilterData(state, filterData) {
|
||||||
state.filterData = filterData
|
state.filterData = filterData
|
||||||
},
|
},
|
||||||
|
setNumUserPlaylists(state, numUserPlaylists) {
|
||||||
|
state.numUserPlaylists = numUserPlaylists
|
||||||
|
},
|
||||||
updateFilterDataWithItem(state, libraryItem) {
|
updateFilterDataWithItem(state, libraryItem) {
|
||||||
if (!libraryItem || !state.filterData) return
|
if (!libraryItem || !state.filterData) return
|
||||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||||
@@ -298,15 +309,6 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSeriesSortBy(state, sortBy) {
|
|
||||||
state.seriesSortBy = sortBy
|
|
||||||
},
|
|
||||||
setSeriesSortDesc(state, sortDesc) {
|
|
||||||
state.seriesSortDesc = sortDesc
|
|
||||||
},
|
|
||||||
setSeriesFilterBy(state, filterBy) {
|
|
||||||
state.seriesFilterBy = filterBy
|
|
||||||
},
|
|
||||||
setCollections(state, collections) {
|
setCollections(state, collections) {
|
||||||
state.collections = collections
|
state.collections = collections
|
||||||
},
|
},
|
||||||
@@ -320,5 +322,22 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
removeCollection(state, collection) {
|
removeCollection(state, collection) {
|
||||||
state.collections = state.collections.filter(c => c.id !== collection.id)
|
state.collections = state.collections.filter(c => c.id !== collection.id)
|
||||||
|
},
|
||||||
|
setUserPlaylists(state, playlists) {
|
||||||
|
state.userPlaylists = playlists
|
||||||
|
state.numUserPlaylists = playlists.length
|
||||||
|
},
|
||||||
|
addUpdateUserPlaylist(state, playlist) {
|
||||||
|
const index = state.userPlaylists.findIndex(p => p.id === playlist.id)
|
||||||
|
if (index >= 0) {
|
||||||
|
state.userPlaylists.splice(index, 1, playlist)
|
||||||
|
} else {
|
||||||
|
state.userPlaylists.push(playlist)
|
||||||
|
state.numUserPlaylists++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeUserPlaylist(state, playlist) {
|
||||||
|
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
|
||||||
|
state.numUserPlaylists = state.userPlaylists.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export const state = () => ({
|
|||||||
value: 'audible.uk'
|
value: 'audible.uk'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Audible.co.au',
|
text: 'Audible.com.au',
|
||||||
value: 'audible.au'
|
value: 'audible.au'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+44
-43
@@ -7,9 +7,12 @@ export const state = () => ({
|
|||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
bookshelfCoverSize: 120,
|
bookshelfCoverSize: 120,
|
||||||
collapseSeries: false,
|
collapseSeries: false,
|
||||||
collapseBookSeries: false
|
collapseBookSeries: false,
|
||||||
},
|
useChapterTrack: false,
|
||||||
settingsListeners: []
|
seriesSortBy: 'name',
|
||||||
|
seriesSortDesc: false,
|
||||||
|
seriesFilterBy: 'all'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -66,7 +69,7 @@ export const getters = {
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
// When changing libraries make sure sort and filter is still valid
|
// When changing libraries make sure sort and filter is still valid
|
||||||
checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) {
|
checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) {
|
||||||
var settingsUpdate = {}
|
const settingsUpdate = {}
|
||||||
if (mediaType == 'podcast') {
|
if (mediaType == 'podcast') {
|
||||||
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.author'
|
settingsUpdate.orderBy = 'media.metadata.author'
|
||||||
@@ -77,8 +80,8 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.title'
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
const invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
||||||
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
settingsUpdate.filterBy = 'all'
|
settingsUpdate.filterBy = 'all'
|
||||||
}
|
}
|
||||||
@@ -94,30 +97,46 @@ export const actions = {
|
|||||||
dispatch('updateUserSettings', settingsUpdate)
|
dispatch('updateUserSettings', settingsUpdate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateUserSettings({ commit }, payload) {
|
updateUserSettings({ state, commit }, payload) {
|
||||||
var updatePayload = {
|
if (!payload) return false
|
||||||
...payload
|
|
||||||
}
|
let hasChanges = false
|
||||||
// Immediately update
|
const existingSettings = { ...state.settings }
|
||||||
commit('setSettings', updatePayload)
|
for (const key in existingSettings) {
|
||||||
return this.$axios.$patch('/api/me/settings', updatePayload).then((result) => {
|
if (payload[key] !== undefined && existingSettings[key] !== payload[key]) {
|
||||||
if (result.success) {
|
hasChanges = true
|
||||||
commit('setSettings', result.settings)
|
existingSettings[key] = payload[key]
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}
|
||||||
console.error('Failed to update settings', error)
|
if (hasChanges) {
|
||||||
return false
|
commit('setSettings', existingSettings)
|
||||||
})
|
this.$eventBus.$emit('user-settings', state.settings)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadUserSettings({ state, commit }) {
|
||||||
|
// Load settings from local storage
|
||||||
|
try {
|
||||||
|
let userSettingsFromLocal = localStorage.getItem('userSettings')
|
||||||
|
if (userSettingsFromLocal) {
|
||||||
|
userSettingsFromLocal = JSON.parse(userSettingsFromLocal)
|
||||||
|
const userSettings = { ...state.settings }
|
||||||
|
for (const key in userSettings) {
|
||||||
|
if (userSettingsFromLocal[key] !== undefined) {
|
||||||
|
userSettings[key] = userSettingsFromLocal[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commit('setSettings', userSettings)
|
||||||
|
this.$eventBus.$emit('user-settings', state.settings)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load userSettings from local storage', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
state.settings = user.settings
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.token) localStorage.setItem('token', user.token)
|
if (user.token) localStorage.setItem('token', user.token)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,25 +162,7 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
setSettings(state, settings) {
|
setSettings(state, settings) {
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
var hasChanges = false
|
localStorage.setItem('userSettings', JSON.stringify(settings))
|
||||||
for (const key in settings) {
|
state.settings = settings
|
||||||
if (state.settings[key] !== settings[key]) {
|
|
||||||
hasChanges = true
|
|
||||||
state.settings[key] = settings[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasChanges) {
|
|
||||||
state.settingsListeners.forEach((listener) => {
|
|
||||||
listener.meth(state.settings)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addSettingsListener(state, listener) {
|
|
||||||
var index = state.settingsListeners.findIndex(l => l.id === listener.id)
|
|
||||||
if (index >= 0) state.settingsListeners.splice(index, 1, listener)
|
|
||||||
else state.settingsListeners.push(listener)
|
|
||||||
},
|
|
||||||
removeSettingsListener(state, listenerId) {
|
|
||||||
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-10
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
users: []
|
usersOnline: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getIsUserOnline: state => id => {
|
getIsUserOnline: state => id => {
|
||||||
return state.users.find(u => u.id === id)
|
return state.usersOnline.find(u => u.id === id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,18 +14,18 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
resetUsers(state) {
|
setUsersOnline(state, usersOnline) {
|
||||||
state.users = []
|
state.usersOnline = usersOnline
|
||||||
},
|
},
|
||||||
updateUser(state, user) {
|
updateUserOnline(state, user) {
|
||||||
var index = state.users.findIndex(u => u.id === user.id)
|
var index = state.usersOnline.findIndex(u => u.id === user.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.users.splice(index, 1, user)
|
state.usersOnline.splice(index, 1, user)
|
||||||
} else {
|
} else {
|
||||||
state.users.push(user)
|
state.usersOnline.push(user)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeUser(state, user) {
|
removeUserOnline(state, user) {
|
||||||
state.users = state.users.filter(u => u.id !== user.id)
|
state.usersOnline = state.usersOnline.filter(u => u.id !== user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+160
-93
@@ -5,7 +5,7 @@
|
|||||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||||
"ButtonApply": "Anwenden",
|
"ButtonApply": "Anwenden",
|
||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
"ButtonAuthors": "Autor",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
"ButtonCancel": "Abbrechen",
|
"ButtonCancel": "Abbrechen",
|
||||||
"ButtonCancelEncode": "Abbruch der Verschlüsselung",
|
"ButtonCancelEncode": "Abbruch der Verschlüsselung",
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
|
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
|
||||||
"ButtonChooseAFolder": "Wähle einen Ordner",
|
"ButtonChooseAFolder": "Wähle einen Ordner",
|
||||||
"ButtonChooseFiles": "Wähle eine Datei",
|
"ButtonChooseFiles": "Wähle eine Datei",
|
||||||
"ButtonClearFilter": "Clear Filter",
|
"ButtonClearFilter": "Filter löschen",
|
||||||
"ButtonCloseFeed": "Feed schließen",
|
"ButtonCloseFeed": "Feed schließen",
|
||||||
"ButtonCollections": "Sammlungen",
|
"ButtonCollections": "Sammlungen",
|
||||||
"ButtonConfigureScanner": "Configure Scanner",
|
"ButtonConfigureScanner": "Scannereinstellungen",
|
||||||
"ButtonCreate": "Ertsellen",
|
"ButtonCreate": "Ertsellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
"ButtonForceReScan": "Erzwinge einen Neu-Scan",
|
"ButtonForceReScan": "Erzwinge kompletten Neu-Scan",
|
||||||
"ButtonFullPath": "Vollständiger Pfad",
|
"ButtonFullPath": "Vollständiger Pfad",
|
||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
@@ -30,22 +30,23 @@
|
|||||||
"ButtonLatest": "Neuste",
|
"ButtonLatest": "Neuste",
|
||||||
"ButtonLibrary": "Bibliothek",
|
"ButtonLibrary": "Bibliothek",
|
||||||
"ButtonLogout": "Abmelden",
|
"ButtonLogout": "Abmelden",
|
||||||
"ButtonLookup": "Nachschlagen",
|
"ButtonLookup": "Online-Suche",
|
||||||
"ButtonManageTracks": "Tracks verwalten",
|
"ButtonManageTracks": "Tracks verwalten",
|
||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Abgleich aller Autoren",
|
"ButtonMatchAllAuthors": "Online-Suche für alle Autoren",
|
||||||
"ButtonMatchBooks": "Abgleich der Bücher",
|
"ButtonMatchBooks": "Online-Suche für alle Hörbücher",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Vergiss es",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOpenManager": "Manager öffnen",
|
"ButtonOpenManager": "Manager öffnen",
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Abspielen",
|
||||||
"ButtonPlaying": "Playing",
|
"ButtonPlaying": "Spielt",
|
||||||
|
"ButtonPlaylists": "Playlists",
|
||||||
"ButtonPurgeAllCache": "Bereinige alle Zwischenspeicher",
|
"ButtonPurgeAllCache": "Bereinige alle Zwischenspeicher",
|
||||||
"ButtonPurgeItemsCache": "Bereinige den Hörbuch/Podcast-Zwischenspeicher",
|
"ButtonPurgeItemsCache": "Bereinige den Hörbuch/Podcast-Zwischenspeicher",
|
||||||
"ButtonPurgeMediaProgress": "Bereinige die Hörfortschritte",
|
"ButtonPurgeMediaProgress": "Bereinige die Hörfortschritte",
|
||||||
"ButtonQueueAddItem": "Add to queue",
|
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||||
"ButtonQueueRemoveItem": "Remove from queue",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
"ButtonRead": "Lese",
|
"ButtonRead": "Lese",
|
||||||
"ButtonRemove": "Löschen",
|
"ButtonRemove": "Löschen",
|
||||||
@@ -60,18 +61,19 @@
|
|||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||||
"ButtonScan": "Scan",
|
"ButtonScan": "Scan",
|
||||||
"ButtonScanLibrary": "Scan Library",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serien",
|
||||||
"ButtonShiftTimes": "Arbeitszeiten",
|
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
||||||
|
"ButtonShiftTimes": "Zeitverschiebung",
|
||||||
"ButtonShow": "Anzeigen",
|
"ButtonShow": "Anzeigen",
|
||||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||||
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
||||||
"ButtonSubmit": "Absenden",
|
"ButtonSubmit": "Ok",
|
||||||
"ButtonUpload": "Hochladen",
|
"ButtonUpload": "Hochladen",
|
||||||
"ButtonUploadBackup": "Sicherung hochladen",
|
"ButtonUploadBackup": "Sicherung hochladen",
|
||||||
"ButtonUploadCover": "Cover hochladen",
|
"ButtonUploadCover": "Titelbild hochladen",
|
||||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
@@ -79,30 +81,34 @@
|
|||||||
"HeaderAdvanced": "Erweitert",
|
"HeaderAdvanced": "Erweitert",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
|
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
|
||||||
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools",
|
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools",
|
||||||
"HeaderAudioTracks": "Audio-Tracks",
|
"HeaderAudioTracks": "Audiodateien",
|
||||||
"HeaderBackups": "Sicherungen",
|
"HeaderBackups": "Sicherungen",
|
||||||
"HeaderChangePassword": "Passwort ändern",
|
"HeaderChangePassword": "Passwort ändern",
|
||||||
"HeaderChapters": "Kapitel",
|
"HeaderChapters": "Kapitel",
|
||||||
"HeaderChooseAFolder": "Wähle einen Ordner",
|
"HeaderChooseAFolder": "Wähle einen Ordner",
|
||||||
"HeaderCollection": "Sammlungen",
|
"HeaderCollection": "Sammlungen",
|
||||||
"HeaderCollectionItems": "Sammlungseinträge",
|
"HeaderCollectionItems": "Sammlungseinträge",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Titelbild",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
"HeaderItemFiles": "Objekt-Dateien",
|
"HeaderItemFiles": "Objekt-Dateien",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Letzte Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
"HeaderLibraryFiles": "Bibliotheksdateien",
|
"HeaderLibraryFiles": "Alle Dateien",
|
||||||
"HeaderLibraryStats": "Bibliotheksstatistiken",
|
"HeaderLibraryStats": "Bibliotheksstatistiken",
|
||||||
"HeaderListeningSessions": "Hörsitzungen",
|
"HeaderListeningSessions": "Ereignisse",
|
||||||
"HeaderListeningStats": "Hörstatistiken",
|
"HeaderListeningStats": "Hörstatistiken",
|
||||||
"HeaderLogin": "Anmeldung",
|
"HeaderLogin": "Anmeldung",
|
||||||
"HeaderLogs": "Protokolle",
|
"HeaderLogs": "Protokolle",
|
||||||
"HeaderMatch": "Übereinstimmung",
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
|
"HeaderMatch": "Online-Suche",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@@ -110,9 +116,11 @@
|
|||||||
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
|
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
|
||||||
"HeaderOtherFiles": "Sonstige Dateien",
|
"HeaderOtherFiles": "Sonstige Dateien",
|
||||||
"HeaderPermissions": "Berechtigungen",
|
"HeaderPermissions": "Berechtigungen",
|
||||||
"HeaderPlayerQueue": "Player Queue",
|
"HeaderPlayerQueue": "Spieler Warteschlange",
|
||||||
|
"HeaderPlaylist": "Playlist",
|
||||||
|
"HeaderPlaylistItems": "Playlist Items",
|
||||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||||
"HeaderPreviewCover": "Vorschau Cover",
|
"HeaderPreviewCover": "Vorschau Titelbild",
|
||||||
"HeaderRemoveEpisode": "Episode löschen",
|
"HeaderRemoveEpisode": "Episode löschen",
|
||||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
@@ -129,7 +137,7 @@
|
|||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Einschlaf-Timer",
|
||||||
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
"HeaderStatsRecentSessions": "Neueste Sitzungen",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
||||||
"HeaderTools": "Werkzeuge",
|
"HeaderTools": "Werkzeuge",
|
||||||
@@ -138,20 +146,23 @@
|
|||||||
"HeaderUpdateDetails": "Details aktualisieren",
|
"HeaderUpdateDetails": "Details aktualisieren",
|
||||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||||
"HeaderUsers": "Benutzer",
|
"HeaderUsers": "Benutzer",
|
||||||
"HeaderYourStats": "Deine Statistiken",
|
"HeaderYourStats": "Eigene Statistik",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
"LabelAccountTypeUser": "Benutzer",
|
"LabelAccountTypeUser": "Benutzer",
|
||||||
"LabelActivity": "Aktivitäten",
|
"LabelActivity": "Aktivitäten",
|
||||||
"LabelAddedAt": "Added At",
|
"LabelAddedAt": "Hinzugefügt am",
|
||||||
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
||||||
"LabelAddToCollectionBatch": "Füge {0} Bücher der Sammlung hinzu",
|
"LabelAddToCollectionBatch": "Füge {0} Bücher der Sammlung hinzu",
|
||||||
"LabelAll": "All",
|
"LabelAddToPlaylist": "Add to Playlist",
|
||||||
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
||||||
"LabelAuthors": "Autoren",
|
"LabelAuthors": "Autoren",
|
||||||
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
|
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
|
||||||
"LabelBackToUser": "Zurück zum Benutzer",
|
"LabelBackToUser": "Zurück zum Benutzer",
|
||||||
@@ -165,6 +176,7 @@
|
|||||||
"LabelChangePassword": "Passwort ändern",
|
"LabelChangePassword": "Passwort ändern",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
|
"LabelClosePlayer": "Player schließen",
|
||||||
"LabelCollapseSeries": "Serien zusammenfassen",
|
"LabelCollapseSeries": "Serien zusammenfassen",
|
||||||
"LabelCollections": "Sammlungen",
|
"LabelCollections": "Sammlungen",
|
||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
@@ -194,18 +206,18 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExplicit": "Ausdrücklich",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
"LabelFileBirthtime": "File Birthtime",
|
"LabelFileBirthtime": "Datei Geburtsdatum",
|
||||||
"LabelFileModified": "File Modified",
|
"LabelFileModified": "Datei geändert",
|
||||||
"LabelFilename": "Dateiname",
|
"LabelFilename": "Dateiname",
|
||||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||||
"LabelFindEpisodes": "Episoden suchen",
|
"LabelFindEpisodes": "Episoden suchen",
|
||||||
"LabelFinished": "Beendet",
|
"LabelFinished": "beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFolders": "Verzeichnisse",
|
"LabelFolders": "Verzeichnisse",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
@@ -214,8 +226,16 @@
|
|||||||
"LabelIncomplete": "Unvollständig",
|
"LabelIncomplete": "Unvollständig",
|
||||||
"LabelInProgress": "In Bearbeitung",
|
"LabelInProgress": "In Bearbeitung",
|
||||||
"LabelInterval": "Intervall",
|
"LabelInterval": "Intervall",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Benutzerdefiniert Täglich/Wöchentlich",
|
||||||
|
"LabelIntervalEvery12Hours": "Alle 12 Stunden",
|
||||||
|
"LabelIntervalEvery15Minutes": "Alle 15 Minuten",
|
||||||
|
"LabelIntervalEvery2Hours": "Alle 2 Stunden",
|
||||||
|
"LabelIntervalEvery30Minutes": "Alle 30 Minuten",
|
||||||
|
"LabelIntervalEvery6Hours": "Alle 6 Stunden",
|
||||||
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
"LabelItem": "Element/Eintrag",
|
"LabelItem": "Hörbuch/Podcast",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
@@ -228,8 +248,11 @@
|
|||||||
"LabelLibraryName": "Bibliotheksname",
|
"LabelLibraryName": "Bibliotheksname",
|
||||||
"LabelLimit": "Begrenzung",
|
"LabelLimit": "Begrenzung",
|
||||||
"LabelListenAgain": "Erneut anhören",
|
"LabelListenAgain": "Erneut anhören",
|
||||||
|
"LabelLogLevelDebug": "Fehlersuche",
|
||||||
|
"LabelLogLevelInfo": "Informationen",
|
||||||
|
"LabelLogLevelWarn": "Warnungen",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
||||||
"LabelMarkSeries": "Serien markieren",
|
"LabelMarkSeries": "Serien markieren als",
|
||||||
"LabelMediaPlayer": "Mediaplayer",
|
"LabelMediaPlayer": "Mediaplayer",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMetadataProvider": "Metadatenanbieter",
|
"LabelMetadataProvider": "Metadatenanbieter",
|
||||||
@@ -239,14 +262,14 @@
|
|||||||
"LabelMissingParts": "Fehlende Teile",
|
"LabelMissingParts": "Fehlende Teile",
|
||||||
"LabelMore": "Mehr",
|
"LabelMore": "Mehr",
|
||||||
"LabelName": "Name",
|
"LabelName": "Name",
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
"LabelNew": "Neu",
|
"LabelNew": "Neu",
|
||||||
"LabelNewestAuthors": "Neuste Autoren",
|
"LabelNewestAuthors": "Neuste Autoren",
|
||||||
"LabelNewestEpisodes": "Neueste Episoden",
|
"LabelNewestEpisodes": "Neueste Episoden",
|
||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Hinweise",
|
||||||
"LabelNotFinished": "Nicht Beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
|
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
|
||||||
"LabelNotificationBodyTemplate": "Textvorlage",
|
"LabelNotificationBodyTemplate": "Textvorlage",
|
||||||
@@ -256,45 +279,49 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Maximale Größe der Warteschlange für die Benachrichtigungsereignisse",
|
"LabelNotificationsMaxQueueSize": "Maximale Größe der Warteschlange für die Benachrichtigungsereignisse",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Es wird nur 1 Ereignis pro Sekunde ausgelöst. Ereignisse werden ignoriert, wenn die Warteschlange die maximale Größe erreicht hat. Dies verhindert Benachrichtigungsspamming.",
|
"LabelNotificationsMaxQueueSizeHelp": "Es wird nur 1 Ereignis pro Sekunde ausgelöst. Ereignisse werden ignoriert, wenn die Warteschlange die maximale Größe erreicht hat. Dies verhindert Benachrichtigungsspamming.",
|
||||||
"LabelNotificationTitleTemplate": "Titelvorlage",
|
"LabelNotificationTitleTemplate": "Titelvorlage",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Nicht begonnen",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
||||||
|
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
||||||
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
||||||
|
"LabelOverwrite": "Überschreiben",
|
||||||
"LabelPassword": "Passwort",
|
"LabelPassword": "Passwort",
|
||||||
"LabelPath": "Pfad",
|
"LabelPath": "Pfad",
|
||||||
"LabelPermissionsAccessAllLibraries": "Darf auf alle Bibliotheken zugreifen",
|
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
||||||
"LabelPermissionsAccessAllTags": "Darf auf alle Schlagwörter zugreifen",
|
"LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter",
|
||||||
"LabelPermissionsAccessExplicitContent": "Darf auf explizite Inhalte zugreifen",
|
"LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte",
|
||||||
"LabelPermissionsDelete": "Darf löschen",
|
"LabelPermissionsDelete": "Löschen",
|
||||||
"LabelPermissionsDownload": "Darf herunterladen",
|
"LabelPermissionsDownload": "Herunterladen",
|
||||||
"LabelPermissionsUpdate": "Darf aktualisieren",
|
"LabelPermissionsUpdate": "Aktualisieren",
|
||||||
"LabelPermissionsUpload": "Darf hochladen",
|
"LabelPermissionsUpload": "Hochladen",
|
||||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||||
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende Vorwort/Artikel (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Erscheinungsjahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
|
"LabelRSSFeedOpen": "RSS Feed Offen",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Begriff suchen",
|
"LabelSearchTerm": "Begriff suchen",
|
||||||
"LabelSearchTitle": "Titel suchen",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||||
"LabelSeason": "Staffel",
|
"LabelSeason": "Staffel",
|
||||||
"LabelSequence": "Reihenfolge",
|
"LabelSequence": "Reihenfolge",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast-unterstützung",
|
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||||
"LabelSettingsDateFormat": "Datumsformat",
|
"LabelSettingsDateFormat": "Datumsformat",
|
||||||
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
||||||
@@ -304,7 +331,7 @@
|
|||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Hörbuch kein eingebettetes Cover oder ein Coverbild im Ordner hat, versucht der Scanner, ein Cover zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn Ihr Hörbuch kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||||
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||||
@@ -315,25 +342,26 @@
|
|||||||
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten aus dem Hörbuchordner",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "In OPF Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Metadaten eines Hörbuchs verwendet. OPF Datein sind seperate \"Textdateien \" mit der Endung \".abs\" in denen verschiedene Matadaten gespiechert sind. Wenn keine OPF Dateien zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten eines Hörbuchs verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Hörbuch selber. In dieser sind verschiedene Matadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird standardmäßig der Ordnername verwendet.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Abgleich alle Bücher die bereits eine ASIN haben",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Abgleich alle Bücher die bereits eine ISBN haben",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Hörbuchtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Hörbuchtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
||||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Titelbilder in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird nur eine Datei mit dem Namen \"cover\" gespeichert.",
|
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Metadaten in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird eine Datei mit der Endung \".abs\" gespeichert.",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Gestartet",
|
"LabelStarted": "Gestartet",
|
||||||
"LabelStartedAt": "Gestartet am",
|
"LabelStartedAt": "Gestartet am",
|
||||||
"LabelStartTime": "Startzeit",
|
"LabelStartTime": "Startzeit",
|
||||||
"LabelStatsAudioTracks": "Audio Tracks",
|
"LabelStatsAudioTracks": "Audiodateien",
|
||||||
"LabelStatsAuthors": "Autoren",
|
"LabelStatsAuthors": "Autoren",
|
||||||
"LabelStatsBestDay": "Bester Tag",
|
"LabelStatsBestDay": "Bester Tag",
|
||||||
"LabelStatsDailyAverage": "Tagesdurchschnitt",
|
"LabelStatsDailyAverage": "Tagesdurchschnitt",
|
||||||
@@ -345,8 +373,8 @@
|
|||||||
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
||||||
"LabelStatsMinutes": "Minuten",
|
"LabelStatsMinutes": "Minuten",
|
||||||
"LabelStatsMinutesListening": "Gehörte Minuten",
|
"LabelStatsMinutesListening": "Gehörte Minuten",
|
||||||
"LabelStatsOverallDays": "Overall Days",
|
"LabelStatsOverallDays": "Gesamte Tage",
|
||||||
"LabelStatsOverallHours": "Overall Hours",
|
"LabelStatsOverallHours": "Gesamte Stunden",
|
||||||
"LabelStatsWeekListening": "Gehörte Wochen",
|
"LabelStatsWeekListening": "Gehörte Wochen",
|
||||||
"LabelSubtitle": "Untertitel",
|
"LabelSubtitle": "Untertitel",
|
||||||
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
|
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
|
||||||
@@ -358,15 +386,19 @@
|
|||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelToolsEmbedMetadata": "Embed Metadata",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
||||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
||||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Hörbuchdastei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
||||||
|
"LabelTotalDuration": "Gesamtdauer",
|
||||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||||
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
||||||
|
"LabelTracks": "Dateien",
|
||||||
|
"LabelTracksMultiTrack": "Mehrfachdatei",
|
||||||
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnknown": "Unbekannt",
|
"LabelUnknown": "Unbekannt",
|
||||||
"LabelUpdateCover": "Titelbild aktualisieren",
|
"LabelUpdateCover": "Titelbild aktualisieren",
|
||||||
@@ -376,25 +408,33 @@
|
|||||||
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
||||||
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
||||||
"LabelUploaderDropFiles": "Dateien löschen",
|
"LabelUploaderDropFiles": "Dateien löschen",
|
||||||
"LabelUseChapterTrack": "Kapitelverfolgung verwenden",
|
"LabelUseChapterTrack": "Kapiteldatei verwenden",
|
||||||
"LabelUseFullTrack": "Gesamten Track verwenden",
|
"LabelUseFullTrack": "Gesamte Datei verwenden",
|
||||||
"LabelUser": "Benutzer",
|
"LabelUser": "Benutzer",
|
||||||
"LabelUsername": "Benutzername",
|
"LabelUsername": "Benutzername",
|
||||||
"LabelValue": "Wert",
|
"LabelValue": "Wert",
|
||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
|
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
||||||
|
"LabelViewChapters": "Kapitel anzeigen",
|
||||||
|
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||||
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
||||||
"LabelYourBookmarks": "Ihre Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourProgress": "Ihre Fortschritte",
|
"LabelYourPlaylists": "Eigene Playlists",
|
||||||
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageBackupsDescription": "In Sicherungen werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder gespeichert",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageBackupsNote": "Die Sicherungen enthalten keine Dateien welche in Ihren Bibliotheksordnern gespeichert sind.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Hörbuch-/Podcastordnern) gespeichert sind.",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoSeries": "You have no series",
|
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Das erste Kapitel muss bei 0 beginnen",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Die ungültige Startzeit darf nicht größer als die gesamte Hörbuchdauer sein",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Die ungültige Startzeit darf nicht größer oder gleich der Startzeit des vorherigen Kapitels sein",
|
||||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
||||||
"MessageCheckingCron": "Überprüfe cron...",
|
"MessageCheckingCron": "Überprüfe cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
@@ -404,18 +444,26 @@
|
|||||||
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
||||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
||||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||||
"MessageFetching": "Abrufen...",
|
"MessageFetching": "Abrufen...",
|
||||||
"MessageForceReScanDescription": "scannt alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu gescannt.",
|
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||||
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} Hörsitzungen im letzten Jahr",
|
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||||
"MessageLoading": "Laden...",
|
"MessageLoading": "Laden...",
|
||||||
"MessageLoadingFolders": "Lade Ordner...",
|
"MessageLoadingFolders": "Lade Ordner...",
|
||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
@@ -424,7 +472,7 @@
|
|||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||||
"MessageNoAudioTracks": "Keine Audiotracks",
|
"MessageNoAudioTracks": "Keine Audiodateien",
|
||||||
"MessageNoAuthors": "Keine Autoren",
|
"MessageNoAuthors": "Keine Autoren",
|
||||||
"MessageNoBackups": "Keine Sicherungen",
|
"MessageNoBackups": "Keine Sicherungen",
|
||||||
"MessageNoBookmarks": "Keine Lesezeichen",
|
"MessageNoBookmarks": "Keine Lesezeichen",
|
||||||
@@ -436,7 +484,7 @@
|
|||||||
"MessageNoEpisodes": "Keine Episoden",
|
"MessageNoEpisodes": "Keine Episoden",
|
||||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||||
"MessageNoGenres": "Keine Kategorien",
|
"MessageNoGenres": "Keine Kategorien",
|
||||||
"MessageNoIssues": "No Issues",
|
"MessageNoIssues": "Keine Probleme",
|
||||||
"MessageNoItems": "Keine Elemente/Einträge",
|
"MessageNoItems": "Keine Elemente/Einträge",
|
||||||
"MessageNoItemsFound": "Keine Elemente/Einträge gefunden",
|
"MessageNoItemsFound": "Keine Elemente/Einträge gefunden",
|
||||||
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
||||||
@@ -446,20 +494,30 @@
|
|||||||
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
||||||
"MessageNoResults": "Keine Ergebnisse",
|
"MessageNoResults": "Keine Ergebnisse",
|
||||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNoSeries": "Keine Serien",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
|
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||||
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageOr": "or",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Abgleich verwendet werden kann",
|
"MessageOr": "oder",
|
||||||
|
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||||
|
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
||||||
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Übereinstimmungen, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
||||||
|
"MessageRemoveChapter": "Kapitel löschen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen Remove from player queue",
|
||||||
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
|
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
||||||
|
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?",
|
||||||
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
|
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
|
||||||
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
||||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
|
||||||
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
|
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
|
||||||
"MessageThinking": "Nachdenken...",
|
"MessageThinking": "Nachdenken...",
|
||||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||||
@@ -467,7 +525,7 @@
|
|||||||
"MessageUploading": "Hochladen...",
|
"MessageUploading": "Hochladen...",
|
||||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Library is empty!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
||||||
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
||||||
@@ -481,6 +539,7 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
||||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
|
"PlaceholderNewPlaylist": "New playlist name",
|
||||||
"PlaceholderSearch": "Suche...",
|
"PlaceholderSearch": "Suche...",
|
||||||
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
||||||
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
||||||
@@ -505,6 +564,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
||||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||||
|
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||||
|
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
|
||||||
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
||||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||||
@@ -528,6 +589,12 @@
|
|||||||
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
||||||
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Playlist aktualisieren",
|
||||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||||
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
||||||
@@ -536,9 +603,9 @@
|
|||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||||
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
||||||
"WeekdayFriday": "Freitag",
|
"WeekdayFriday": "Freitag",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"ButtonOpenManager": "Open Manager",
|
"ButtonOpenManager": "Open Manager",
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Play",
|
||||||
"ButtonPlaying": "Playing",
|
"ButtonPlaying": "Playing",
|
||||||
|
"ButtonPlaylists": "Playlists",
|
||||||
"ButtonPurgeAllCache": "Purge All Cache",
|
"ButtonPurgeAllCache": "Purge All Cache",
|
||||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"ButtonSearch": "Search",
|
"ButtonSearch": "Search",
|
||||||
"ButtonSelectFolderPath": "Select Folder Path",
|
"ButtonSelectFolderPath": "Select Folder Path",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
|
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||||
"ButtonShiftTimes": "Shift Times",
|
"ButtonShiftTimes": "Shift Times",
|
||||||
"ButtonShow": "Show",
|
"ButtonShow": "Show",
|
||||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
"HeaderItemFiles": "Item Files",
|
"HeaderItemFiles": "Item Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Last Listening Session",
|
"HeaderLastListeningSession": "Last Listening Session",
|
||||||
"HeaderLatestEpisodes": "Latest episodes",
|
"HeaderLatestEpisodes": "Latest episodes",
|
||||||
"HeaderLibraries": "Libraries",
|
"HeaderLibraries": "Libraries",
|
||||||
@@ -102,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Listening Stats",
|
"HeaderListeningStats": "Listening Stats",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Match",
|
"HeaderMatch": "Match",
|
||||||
"HeaderMetadataToEmbed": "Metadata to embed",
|
"HeaderMetadataToEmbed": "Metadata to embed",
|
||||||
"HeaderNewAccount": "New Account",
|
"HeaderNewAccount": "New Account",
|
||||||
@@ -111,6 +117,8 @@
|
|||||||
"HeaderOtherFiles": "Other Files",
|
"HeaderOtherFiles": "Other Files",
|
||||||
"HeaderPermissions": "Permissions",
|
"HeaderPermissions": "Permissions",
|
||||||
"HeaderPlayerQueue": "Player Queue",
|
"HeaderPlayerQueue": "Player Queue",
|
||||||
|
"HeaderPlaylist": "Playlist",
|
||||||
|
"HeaderPlaylistItems": "Playlist Items",
|
||||||
"HeaderPodcastsToAdd": "Podcasts to Add",
|
"HeaderPodcastsToAdd": "Podcasts to Add",
|
||||||
"HeaderPreviewCover": "Preview Cover",
|
"HeaderPreviewCover": "Preview Cover",
|
||||||
"HeaderRemoveEpisode": "Remove Episode",
|
"HeaderRemoveEpisode": "Remove Episode",
|
||||||
@@ -147,8 +155,11 @@
|
|||||||
"LabelAddedAt": "Added At",
|
"LabelAddedAt": "Added At",
|
||||||
"LabelAddToCollection": "Add to Collection",
|
"LabelAddToCollection": "Add to Collection",
|
||||||
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
||||||
|
"LabelAddToPlaylist": "Add to Playlist",
|
||||||
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "All Users",
|
"LabelAllUsers": "All Users",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Author",
|
"LabelAuthor": "Author",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
@@ -165,6 +176,7 @@
|
|||||||
"LabelChangePassword": "Change Password",
|
"LabelChangePassword": "Change Password",
|
||||||
"LabelChaptersFound": "chapters found",
|
"LabelChaptersFound": "chapters found",
|
||||||
"LabelChapterTitle": "Chapter Title",
|
"LabelChapterTitle": "Chapter Title",
|
||||||
|
"LabelClosePlayer": "Close player",
|
||||||
"LabelCollapseSeries": "Collapse Series",
|
"LabelCollapseSeries": "Collapse Series",
|
||||||
"LabelCollections": "Collections",
|
"LabelCollections": "Collections",
|
||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
@@ -214,6 +226,14 @@
|
|||||||
"LabelIncomplete": "Incomplete",
|
"LabelIncomplete": "Incomplete",
|
||||||
"LabelInProgress": "In Progress",
|
"LabelInProgress": "In Progress",
|
||||||
"LabelInterval": "Interval",
|
"LabelInterval": "Interval",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
|
||||||
|
"LabelIntervalEvery12Hours": "Every 12 hours",
|
||||||
|
"LabelIntervalEvery15Minutes": "Every 15 minutes",
|
||||||
|
"LabelIntervalEvery2Hours": "Every 2 hours",
|
||||||
|
"LabelIntervalEvery30Minutes": "Every 30 minutes",
|
||||||
|
"LabelIntervalEvery6Hours": "Every 6 hours",
|
||||||
|
"LabelIntervalEveryDay": "Every day",
|
||||||
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
@@ -228,6 +248,9 @@
|
|||||||
"LabelLibraryName": "Library Name",
|
"LabelLibraryName": "Library Name",
|
||||||
"LabelLimit": "Limit",
|
"LabelLimit": "Limit",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
"LabelLogLevelInfo": "Info",
|
||||||
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMarkSeries": "Mark Series",
|
"LabelMarkSeries": "Mark Series",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
@@ -257,8 +280,10 @@
|
|||||||
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
|
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
|
||||||
"LabelNotificationTitleTemplate": "Title Template",
|
"LabelNotificationTitleTemplate": "Title Template",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
|
"LabelNumberOfBooks": "Number of Books",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfEpisodes": "# of Episodes",
|
||||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Path",
|
"LabelPath": "Path",
|
||||||
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
||||||
@@ -269,6 +294,7 @@
|
|||||||
"LabelPermissionsUpdate": "Can Update",
|
"LabelPermissionsUpdate": "Can Update",
|
||||||
"LabelPermissionsUpload": "Can Upload",
|
"LabelPermissionsUpload": "Can Upload",
|
||||||
"LabelPhotoPathURL": "Photo Path/URL",
|
"LabelPhotoPathURL": "Photo Path/URL",
|
||||||
|
"LabelPlaylists": "Playlists",
|
||||||
"LabelPlayMethod": "Play Method",
|
"LabelPlayMethod": "Play Method",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
@@ -282,6 +308,7 @@
|
|||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
|
"LabelRemoveCover": "Remove cover",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
@@ -321,7 +348,7 @@
|
|||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
"LabelSettingsSquareBookCovers": "Use square book covers",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||||
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
||||||
@@ -329,6 +356,7 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
||||||
"LabelShowAll": "Show All",
|
"LabelShowAll": "Show All",
|
||||||
"LabelSize": "Size",
|
"LabelSize": "Size",
|
||||||
|
"LabelSleepTimer": "Sleep timer",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Started",
|
"LabelStarted": "Started",
|
||||||
"LabelStartedAt": "Started At",
|
"LabelStartedAt": "Started At",
|
||||||
@@ -364,9 +392,13 @@
|
|||||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
||||||
|
"LabelTotalDuration": "Total Duration",
|
||||||
"LabelTotalTimeListened": "Total Time Listened",
|
"LabelTotalTimeListened": "Total Time Listened",
|
||||||
"LabelTrackFromFilename": "Track from Filename",
|
"LabelTrackFromFilename": "Track from Filename",
|
||||||
"LabelTrackFromMetadata": "Track from Metadata",
|
"LabelTrackFromMetadata": "Track from Metadata",
|
||||||
|
"LabelTracks": "Tracks",
|
||||||
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnknown": "Unknown",
|
"LabelUnknown": "Unknown",
|
||||||
"LabelUpdateCover": "Update Cover",
|
"LabelUpdateCover": "Update Cover",
|
||||||
@@ -382,19 +414,27 @@
|
|||||||
"LabelUsername": "Username",
|
"LabelUsername": "Username",
|
||||||
"LabelValue": "Value",
|
"LabelValue": "Value",
|
||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
|
"LabelViewBookmarks": "View bookmarks",
|
||||||
|
"LabelViewChapters": "View chapters",
|
||||||
|
"LabelViewQueue": "View player queue",
|
||||||
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdays to run",
|
"LabelWeekdaysToRun": "Weekdays to run",
|
||||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||||
"LabelYourBookmarks": "Your Bookmarks",
|
"LabelYourBookmarks": "Your Bookmarks",
|
||||||
|
"LabelYourPlaylists": "Your Playlists",
|
||||||
"LabelYourProgress": "Your Progress",
|
"LabelYourProgress": "Your Progress",
|
||||||
|
"MessageAddToPlayerQueue": "Add to player queue",
|
||||||
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in",
|
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
|
||||||
"MessageBackupsNote": "Backups do not include any files stored in your library folders.",
|
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||||
"MessageBookshelfNoSeries": "You have no series",
|
"MessageBookshelfNoSeries": "You have no series",
|
||||||
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
|
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
|
||||||
|
"MessageChapterErrorFirstNotZero": "First chapter must start at 0",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
||||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||||
"MessageCheckingCron": "Checking cron...",
|
"MessageCheckingCron": "Checking cron...",
|
||||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||||
@@ -404,6 +444,13 @@
|
|||||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -414,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Important Notice!",
|
"MessageImportantNotice": "Important Notice!",
|
||||||
"MessageInsertChapterBelow": "Insert chapter below",
|
"MessageInsertChapterBelow": "Insert chapter below",
|
||||||
"MessageItemsSelected": "{0} Items Selected",
|
"MessageItemsSelected": "{0} Items Selected",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Join us on",
|
"MessageJoinUsOn": "Join us on",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
||||||
"MessageLoading": "Loading...",
|
"MessageLoading": "Loading...",
|
||||||
@@ -446,20 +494,30 @@
|
|||||||
"MessageNoPodcastsFound": "No podcasts found",
|
"MessageNoPodcastsFound": "No podcasts found",
|
||||||
"MessageNoResults": "No Results",
|
"MessageNoResults": "No Results",
|
||||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||||
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageNoUpdateNecessary": "No update necessary",
|
"MessageNoUpdateNecessary": "No update necessary",
|
||||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||||
|
"MessageNoUserPlaylists": "You have no playlists",
|
||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
||||||
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
||||||
|
"MessageRemoveChapter": "Remove chapter",
|
||||||
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||||
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
|
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
|
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
|
||||||
|
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||||
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
||||||
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
||||||
"MessageSearchResultsFor": "Search results for",
|
"MessageSearchResultsFor": "Search results for",
|
||||||
"MessageServerCouldNotBeReached": "Server could not be reached",
|
"MessageServerCouldNotBeReached": "Server could not be reached",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||||
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
|
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
|
||||||
"MessageThinking": "Thinking...",
|
"MessageThinking": "Thinking...",
|
||||||
"MessageUploaderItemFailed": "Failed to upload",
|
"MessageUploaderItemFailed": "Failed to upload",
|
||||||
@@ -481,6 +539,7 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
|
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
|
||||||
"PlaceholderNewCollection": "New collection name",
|
"PlaceholderNewCollection": "New collection name",
|
||||||
"PlaceholderNewFolderPath": "New folder path",
|
"PlaceholderNewFolderPath": "New folder path",
|
||||||
|
"PlaceholderNewPlaylist": "New playlist name",
|
||||||
"PlaceholderSearch": "Search..",
|
"PlaceholderSearch": "Search..",
|
||||||
"ToastAccountUpdateFailed": "Failed to update account",
|
"ToastAccountUpdateFailed": "Failed to update account",
|
||||||
"ToastAccountUpdateSuccess": "Account updated",
|
"ToastAccountUpdateSuccess": "Account updated",
|
||||||
@@ -505,6 +564,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
||||||
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
|
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
|
||||||
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
||||||
|
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||||
|
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||||
"ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
|
"ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
||||||
"ToastCollectionRemoveFailed": "Failed to remove collection",
|
"ToastCollectionRemoveFailed": "Failed to remove collection",
|
||||||
@@ -528,6 +589,12 @@
|
|||||||
"ToastLibraryScanStarted": "Library scan started",
|
"ToastLibraryScanStarted": "Library scan started",
|
||||||
"ToastLibraryUpdateFailed": "Failed to update library",
|
"ToastLibraryUpdateFailed": "Failed to update library",
|
||||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||||
"ToastPodcastCreateFailed": "Failed to create podcast",
|
"ToastPodcastCreateFailed": "Failed to create podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast created successfully",
|
"ToastPodcastCreateSuccess": "Podcast created successfully",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
|
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user