mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea60f19e7a | |||
| cefc75a4ed | |||
| d8753aafb9 | |||
| ba5ad228cc | |||
| 0203f9cc1b | |||
| 4770be5a39 | |||
| 1bac395bed | |||
| e818f270cd | |||
| c4e2726622 | |||
| 74d8a09f31 | |||
| 618338165e | |||
| db494001a2 | |||
| a67213fb7b | |||
| 5d96b2cc6e | |||
| 72d0b097ab | |||
| 36d2957fb4 | |||
| b5de517aad | |||
| 41db0e3bb1 | |||
| e8d582269b | |||
| 80ef8ee890 | |||
| a65859f575 | |||
| 5724887785 | |||
| 8908aa7a82 | |||
| f83dd29213 | |||
| 99d90778f4 | |||
| 49279430fc | |||
| 030c20b12e | |||
| 5e943ef152 | |||
| 4ae057f626 | |||
| 9ebe4b55dd | |||
| 2f7403adec | |||
| 2777b496ad | |||
| f7a3dbf209 | |||
| d900093976 | |||
| 08250e266e | |||
| da2d1455d7 | |||
| b6c6c4c939 | |||
| 22179d82b8 | |||
| 343ce312f1 | |||
| 10677d6fb0 | |||
| 49a8aead9b | |||
| 274b0e48be | |||
| 4d8ffc5d99 | |||
| 4f3029e5b2 | |||
| a1b49f5fcf | |||
| 89d497a305 | |||
| 9e095a4bc1 | |||
| 024d052a7b | |||
| c312979aec | |||
| 773e621944 | |||
| ed4f33b565 | |||
| f8a0852dfc | |||
| 6dec750d3e | |||
| 3c98a5fb24 | |||
| 702ee3d350 | |||
| fcc2f3650b | |||
| e4ad622c01 | |||
| 458403eec9 | |||
| aaede2752c | |||
| 39d8c2cf04 | |||
| dd5c940d36 | |||
| 277f024bbc | |||
| 59ad1e5e36 | |||
| 02c4b21d3f | |||
| 33ae5445be | |||
| 5ed06871b6 | |||
| e98eb8f1eb | |||
| ebedaeb3b0 | |||
| 62aec63d1d | |||
| 3c25e87e8d | |||
| 08d16ce7c2 | |||
| 2cb3808326 | |||
| bdb6f0c0aa | |||
| 5255bf13cc | |||
| 3588e1e8d3 | |||
| 8fa8360e99 | |||
| b305cfd268 | |||
| ff10287d05 | |||
| 7a7708403f | |||
| ddabd0ee75 | |||
| 5a26704c32 | |||
| 7ccf36a896 | |||
| e9a84dd7dd | |||
| b00510855e | |||
| 2cd9079692 | |||
| 3e4b1652fc | |||
| 878330b4fb | |||
| 9a85ad1f6b | |||
| f76f9c7f84 | |||
| 3426832f2b | |||
| 10fd51498c | |||
| 49c581ed35 | |||
| f095d89980 | |||
| 1609f1a499 | |||
| 88bd51e2da | |||
| 74388fe0b9 | |||
| 7f5356100d | |||
| 84d2d00a30 | |||
| 31dddfbb60 | |||
| d6da161b13 | |||
| 9de7be1cb4 | |||
| 5410aae8fc | |||
| 86bf6bfc62 | |||
| 0807146aab | |||
| 591d8a8ab1 | |||
| b1d4e28027 | |||
| 44363f05ac | |||
| 452af43916 | |||
| 70ba2f7850 | |||
| a364fe5031 | |||
| ca6765c8e7 | |||
| 6bfa281dc5 | |||
| d8ee61bfab | |||
| c6763dee2d | |||
| 0e6b0d3eff | |||
| 8bbfee334c | |||
| f806e4cce3 | |||
| 209ba308bd | |||
| 4cd9088a66 | |||
| ac5e2e5c73 | |||
| f1329d2847 | |||
| 27faefc64d | |||
| 0fa7e61dc1 | |||
| 5a3f14ae51 | |||
| 4e61185136 | |||
| 6ee06d5dae | |||
| 2c344a0bc0 | |||
| 315c83e4c3 | |||
| 9e4bc582cb | |||
| fc6aa1f91f | |||
| d4bea34423 | |||
| a551a2d288 | |||
| 4b0c59b174 | |||
| a0840d2a08 | |||
| 308ccf470f | |||
| 4021b6eca1 | |||
| 061695f922 | |||
| e803dcd325 | |||
| 128796bd36 | |||
| 775dedc338 | |||
| 45c9038954 | |||
| 8acf962864 | |||
| c3fc38639e | |||
| b60b75c8da | |||
| 0f7edec73b | |||
| 321277826f | |||
| 6e752af2c0 | |||
| 0717ae39db | |||
| 7bc5902ea8 | |||
| a28e1ed5e0 | |||
| 43d9e129a6 | |||
| b516019ddd | |||
| e4c20d677c | |||
| 33e183b802 | |||
| b884f8fe11 | |||
| 2cba83f1dd | |||
| a9ee9031c3 | |||
| c3717f6979 | |||
| 657d4dd705 | |||
| 17356ffd79 | |||
| c4be75b5bd | |||
| 57422d0759 | |||
| d2454201b4 | |||
| 3a92a69693 | |||
| d733c9ccc6 | |||
| 3e15e09c07 | |||
| 0592a41d4f | |||
| c32e33f804 | |||
| 616ffb8f79 | |||
| bc771a3a44 | |||
| 539d1a2d4f | |||
| 4d8cea0bb4 | |||
| 8b46262e93 | |||
| eb9a077520 | |||
| 3d3a224402 | |||
| e1397a6dda | |||
| 8f49aae979 | |||
| c0a13f01d4 | |||
| efcebc616c | |||
| 902867c3bc | |||
| b7abd372e4 | |||
| 147ffc0210 | |||
| 1b2ccb6cee | |||
| 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 |
@@ -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
|
||||||
@@ -54,17 +54,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<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" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
@@ -24,19 +24,25 @@
|
|||||||
<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="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" 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="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" 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="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" 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 sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
<span class="items-center hidden md:flex">
|
<span class="items-center hidden md:flex">
|
||||||
<span class="block truncate">{{ username }}</span>
|
<span class="block truncate">{{ username }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -45,24 +51,24 @@
|
|||||||
</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 text-2xl -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 && isBookLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip v-if="!isPodcastLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
<ui-tooltip v-if="userCanUpdate && isBookLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
||||||
<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>
|
||||||
@@ -97,6 +103,9 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.libraryMediaType === 'book'
|
||||||
|
},
|
||||||
isHome() {
|
isHome() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
@@ -109,11 +118,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 +141,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 +166,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)
|
||||||
@@ -168,12 +184,15 @@ export default {
|
|||||||
|
|
||||||
const queueItems = []
|
const queueItems = []
|
||||||
libraryItems.forEach((item) => {
|
libraryItems.forEach((item) => {
|
||||||
|
let subtitle = ''
|
||||||
|
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
||||||
|
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
|
||||||
queueItems.push({
|
queueItems.push({
|
||||||
libraryItemId: item.id,
|
libraryItemId: item.id,
|
||||||
libraryId: item.libraryId,
|
libraryId: item.libraryId,
|
||||||
episodeId: null,
|
episodeId: null,
|
||||||
title: item.media.metadata.title,
|
title: item.media.metadata.title,
|
||||||
subtitle: item.media.metadata.authors.map((au) => au.name).join(', '),
|
subtitle,
|
||||||
caption: '',
|
caption: '',
|
||||||
duration: item.media.duration || null,
|
duration: item.media.duration || null,
|
||||||
coverPath: item.media.coverPath || null
|
coverPath: item.media.coverPath || null
|
||||||
@@ -185,20 +204,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 +227,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 +237,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) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
|
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
|
||||||
<!-- Cover size widget -->
|
<!-- Cover size widget -->
|
||||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
|
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
@@ -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.audioFile || 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.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -157,8 +167,8 @@ export default {
|
|||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
var categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,17 +16,17 @@
|
|||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -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">
|
||||||
@@ -50,18 +50,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||||
<ui-btn v-if="!isBatchSelecting" color="primary" small :loading="processingSeries" class="items-center ml-1 sm:ml-4 hidden md:flex" @click="markSeriesFinished">
|
|
||||||
<div class="h-5 w-5">
|
<!-- RSS feed -->
|
||||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :size="7" icon-font-size="1.2rem" bg-color="success" outlined @click="showOpenSeriesRSSFeed" />
|
||||||
</svg>
|
</ui-tooltip>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
|
|
||||||
</ui-btn>
|
|
||||||
<ui-btn v-if="isSeriesRemovedFromContinueListening && !isBatchSelecting" small :loading="processingSeries" @click="reAddSeriesToContinueListening" class="hidden md:block ml-2"> Re-Add Series to Continue Listening </ui-btn>
|
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||||
@@ -69,11 +64,11 @@
|
|||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
<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 && isBookLibrary && !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>
|
||||||
@@ -118,6 +113,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
seriesContextMenuItems() {
|
||||||
|
if (!this.selectedSeries) return []
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.isSeriesFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished,
|
||||||
|
action: 'mark-series-finished'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.userIsAdminOrUp || this.selectedSeries.rssFeed) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelOpenRSSFeed,
|
||||||
|
action: 'open-rss-feed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSeriesRemovedFromContinueListening) {
|
||||||
|
items.push({
|
||||||
|
text: 'Re-Add Series to Continue Listening',
|
||||||
|
action: 're-add-to-continue-listening'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
},
|
||||||
seriesSortItems() {
|
seriesSortItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -153,9 +174,15 @@ export default {
|
|||||||
currentLibraryMediaType() {
|
currentLibraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusicLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'music'
|
||||||
|
},
|
||||||
isLibraryPage() {
|
isLibraryPage() {
|
||||||
return this.page === ''
|
return this.page === ''
|
||||||
},
|
},
|
||||||
@@ -180,10 +207,16 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.$route.name === 'library-library-authors'
|
||||||
},
|
},
|
||||||
|
isAlbumsPage() {
|
||||||
|
return this.page === 'albums'
|
||||||
|
},
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.totalEntities
|
return this.totalEntities
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
|
if (this.isAlbumsPage) return 'Albums'
|
||||||
|
if (this.isMusicLibrary) return 'Tracks'
|
||||||
|
|
||||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||||
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
|
||||||
@@ -200,12 +233,15 @@ export default {
|
|||||||
seriesProgress() {
|
seriesProgress() {
|
||||||
return this.selectedSeries ? this.selectedSeries.progress : null
|
return this.selectedSeries ? this.selectedSeries.progress : null
|
||||||
},
|
},
|
||||||
|
seriesRssFeed() {
|
||||||
|
return this.selectedSeries ? this.selectedSeries.rssFeed : null
|
||||||
|
},
|
||||||
seriesLibraryItemIds() {
|
seriesLibraryItemIds() {
|
||||||
if (!this.seriesProgress) return []
|
if (!this.seriesProgress) return []
|
||||||
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
|
||||||
@@ -219,33 +255,34 @@ 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: {
|
||||||
|
seriesContextMenuAction(action) {
|
||||||
|
if (action === 'open-rss-feed') {
|
||||||
|
this.showOpenSeriesRSSFeed()
|
||||||
|
} else if (action === 're-add-to-continue-listening') {
|
||||||
|
if (this.processingSeries) {
|
||||||
|
console.warn('Already processing series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reAddSeriesToContinueListening()
|
||||||
|
} else if (action === 'mark-series-finished') {
|
||||||
|
if (this.processingSeries) {
|
||||||
|
console.warn('Already processing series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.markSeriesFinished()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showOpenSeriesRSSFeed() {
|
||||||
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.selectedSeries.id,
|
||||||
|
name: this.selectedSeries.name,
|
||||||
|
type: 'series',
|
||||||
|
feed: this.selectedSeries.rssFeed
|
||||||
|
})
|
||||||
|
},
|
||||||
reAddSeriesToContinueListening() {
|
reAddSeriesToContinueListening() {
|
||||||
this.processingSeries = true
|
this.processingSeries = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -310,27 +347,38 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
markSeriesFinished() {
|
markSeriesFinished() {
|
||||||
var newIsFinished = !this.isSeriesFinished
|
const newIsFinished = !this.isSeriesFinished
|
||||||
this.processingSeries = true
|
|
||||||
var updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
const payload = {
|
||||||
return {
|
message: newIsFinished ? this.$strings.MessageConfirmMarkSeriesFinished : this.$strings.MessageConfirmMarkSeriesNotFinished,
|
||||||
libraryItemId: lid,
|
callback: (confirmed) => {
|
||||||
isFinished: newIsFinished
|
if (confirmed) {
|
||||||
}
|
this.processingSeries = true
|
||||||
})
|
const updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
||||||
console.log('Progress payloads', updateProgressPayloads)
|
return {
|
||||||
this.$axios
|
libraryItemId: lid,
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
isFinished: newIsFinished
|
||||||
.then(() => {
|
}
|
||||||
this.$toast.success('Series update success')
|
})
|
||||||
this.selectedSeries.progress.isFinished = newIsFinished
|
console.log('Progress payloads', updateProgressPayloads)
|
||||||
this.processingSeries = false
|
this.$axios
|
||||||
})
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.catch((error) => {
|
.then(() => {
|
||||||
this.$toast.error('Series update failed')
|
this.$toast.success(this.$strings.ToastSeriesUpdateSuccess)
|
||||||
console.error('Failed to batch update read/not read', error)
|
this.selectedSeries.progress.isFinished = newIsFinished
|
||||||
this.processingSeries = false
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
this.$toast.error(this.$strings.ToastSeriesUpdateFailed)
|
||||||
|
console.error('Failed to batch update read/not read', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processingSeries = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
@@ -339,10 +387,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()
|
||||||
@@ -363,16 +411,32 @@ export default {
|
|||||||
},
|
},
|
||||||
setBookshelfTotalEntities(totalEntities) {
|
setBookshelfTotalEntities(totalEntities) {
|
||||||
this.totalEntities = totalEntities
|
this.totalEntities = totalEntities
|
||||||
|
},
|
||||||
|
rssFeedOpen(data) {
|
||||||
|
if (data.entityId === this.seriesId) {
|
||||||
|
console.log('RSS Feed Opened', data)
|
||||||
|
this.selectedSeries.rssFeed = data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rssFeedClosed(data) {
|
||||||
|
if (data.entityId === this.seriesId) {
|
||||||
|
console.log('RSS Feed Closed', data)
|
||||||
|
this.selectedSeries.rssFeed = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
||||||
<div class="md:hidden flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ export default {
|
|||||||
var classes = []
|
var classes = []
|
||||||
if (this.drawerOpen) classes.push('translate-x-0')
|
if (this.drawerOpen) classes.push('translate-x-0')
|
||||||
else classes.push('-translate-x-44')
|
else classes.push('-translate-x-44')
|
||||||
if (this.isMobile) classes.push('z-50')
|
if (this.isMobilePortrait) classes.push('z-50')
|
||||||
else classes.push('z-40')
|
else classes.push('z-40')
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
},
|
},
|
||||||
@@ -119,9 +124,11 @@ export default {
|
|||||||
isMobileLandscape() {
|
isMobileLandscape() {
|
||||||
return this.$store.state.globals.isMobileLandscape
|
return this.$store.state.globals.isMobileLandscape
|
||||||
},
|
},
|
||||||
|
isMobilePortrait() {
|
||||||
|
return this.$store.state.globals.isMobilePortrait
|
||||||
|
},
|
||||||
drawerOpen() {
|
drawerOpen() {
|
||||||
if (this.isMobile) return this.isOpen
|
return !this.isMobilePortrait || this.isOpen
|
||||||
return true
|
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||||
<!-- Clear filter only available on Library bookshelf -->
|
<!-- Clear filter only available on Library bookshelf -->
|
||||||
<div v-if="entityName === 'books'" class="flex justify-center mt-2">
|
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
||||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
|
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -81,8 +81,11 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
@@ -96,17 +99,17 @@ export default {
|
|||||||
return this.$strings.MessageNoResults
|
return this.$strings.MessageNoResults
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (!this.page) return 'books'
|
if (!this.page) return 'items'
|
||||||
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')
|
||||||
@@ -158,12 +161,9 @@ export default {
|
|||||||
libraryName() {
|
libraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
isEntityBook() {
|
|
||||||
return this.entityName === 'series-books' || this.entityName === 'books'
|
|
||||||
},
|
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
const 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() {
|
||||||
@@ -192,7 +192,8 @@ export default {
|
|||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
if (this.isAlternativeBookshelfView) {
|
if (this.isAlternativeBookshelfView) {
|
||||||
var extraTitleSpace = this.isEntityBook ? 80 : 40
|
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items'
|
||||||
|
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
|
||||||
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
|
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
|
||||||
}
|
}
|
||||||
return this.entityHeight + 40
|
return this.entityHeight + 40
|
||||||
@@ -201,11 +202,11 @@ 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
|
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.entityWidth / baseSize
|
return this.entityWidth / baseSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -214,8 +215,8 @@ export default {
|
|||||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||||
},
|
},
|
||||||
editEntity(entity) {
|
editEntity(entity) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var bookIds = this.entities.map((e) => e.id)
|
const bookIds = this.entities.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
this.$store.commit('showEditModal', entity)
|
this.$store.commit('showEditModal', entity)
|
||||||
} else if (this.entityName === 'collections') {
|
} else if (this.entityName === 'collections') {
|
||||||
@@ -229,29 +230,29 @@ export default {
|
|||||||
this.isSelectionMode = false
|
this.isSelectionMode = false
|
||||||
},
|
},
|
||||||
selectEntity(entity, shiftKey) {
|
selectEntity(entity, shiftKey) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || 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
|
||||||
}
|
}
|
||||||
@@ -269,16 +270,27 @@ 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.audioFile || 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.audioFile || 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)
|
||||||
@@ -296,7 +308,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchEntites(page = 0) {
|
async fetchEntites(page = 0) {
|
||||||
var startIndex = page * this.booksPerFetch
|
const startIndex = page * this.booksPerFetch
|
||||||
|
|
||||||
this.isFetchingEntities = true
|
this.isFetchingEntities = true
|
||||||
|
|
||||||
@@ -304,9 +316,9 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
|
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
||||||
|
|
||||||
const 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)
|
||||||
@@ -328,7 +340,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < payload.results.length; i++) {
|
for (let i = 0; i < payload.results.length; i++) {
|
||||||
var index = i + startIndex
|
const index = i + startIndex
|
||||||
this.entities[index] = payload.results[i]
|
this.entities[index] = payload.results[i]
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
@@ -486,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) {
|
||||||
@@ -505,7 +517,7 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
console.log('Item updated', libraryItem)
|
console.log('Item updated', libraryItem)
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities[indexOf] = libraryItem
|
this.entities[indexOf] = libraryItem
|
||||||
@@ -516,7 +528,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemRemoved(libraryItem) {
|
libraryItemRemoved(libraryItem) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
||||||
@@ -655,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)
|
||||||
@@ -684,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)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<h1 class="text-xl">{{ headerText }}</h1>
|
<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">
|
<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>
|
<button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</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="isBookLibrary" :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 text-2xl">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>
|
||||||
@@ -50,7 +49,7 @@
|
|||||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" 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="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" 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="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -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="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" 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="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons-outlined text-xl">album</span>
|
||||||
|
|
||||||
|
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||||
|
|
||||||
|
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</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'">
|
<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>
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
@@ -133,15 +140,24 @@ export default {
|
|||||||
currentLibraryMediaType() {
|
currentLibraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusicLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'music'
|
||||||
|
},
|
||||||
isPodcastSearchPage() {
|
isPodcastSearchPage() {
|
||||||
return this.$route.name === 'library-library-podcast-search'
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
},
|
},
|
||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isMusicAlbumsPage() {
|
||||||
|
return this.paramId === 'albums'
|
||||||
|
},
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<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-2 md: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-2 top-2 md:left-4 cursor-pointer">
|
||||||
<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" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-icons text-sm">person</span>
|
||||||
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
||||||
|
<p v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</p>
|
||||||
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
@@ -85,12 +86,15 @@ export default {
|
|||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
isSquareCover() {
|
||||||
return 88
|
return this.coverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverPosTop() {
|
isMobile() {
|
||||||
if (this.coverAspectRatio == 1) return -10
|
return this.$store.state.globals.isMobile
|
||||||
return -64
|
},
|
||||||
|
bookCoverWidth() {
|
||||||
|
if (this.isMobile) return 64 / this.coverAspectRatio
|
||||||
|
return 77 / this.coverAspectRatio
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
if (this.media.coverPath) return this.media.coverPath
|
if (this.media.coverPath) return this.media.coverPath
|
||||||
@@ -122,6 +126,9 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
@@ -145,6 +152,10 @@ export default {
|
|||||||
if (!this.isPodcast) return null
|
if (!this.isPodcast) return null
|
||||||
return this.mediaMetadata.author || 'Unknown'
|
return this.mediaMetadata.author || 'Unknown'
|
||||||
},
|
},
|
||||||
|
musicArtists() {
|
||||||
|
if (!this.isMusic) return null
|
||||||
|
return this.mediaMetadata.artists.join(', ')
|
||||||
|
},
|
||||||
playerQueueItems() {
|
playerQueueItems() {
|
||||||
return this.$store.state.playerQueueItems || []
|
return this.$store.state.playerQueueItems || []
|
||||||
}
|
}
|
||||||
@@ -350,13 +361,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
||||||
var chunks = data.chunks
|
if (!data.numSegments) return
|
||||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
var chunks = data.chunks
|
||||||
if (this.$refs.audioPlayer) {
|
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
if (this.$refs.audioPlayer) {
|
||||||
} else {
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
console.error('No Audio Ref')
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionOpen(session) {
|
sessionOpen(session) {
|
||||||
@@ -405,8 +418,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
var libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId || null
|
const episodeId = payload.episodeId || null
|
||||||
|
|
||||||
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
||||||
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
||||||
@@ -417,11 +430,12 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
const libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed to fetch full item', error)
|
console.error('Failed to fetch full item', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
|
|
||||||
this.$store.commit('setMediaPlaying', {
|
this.$store.commit('setMediaPlaying', {
|
||||||
libraryItem,
|
libraryItem,
|
||||||
episodeId,
|
episodeId,
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,6 +67,7 @@ export default {
|
|||||||
// but with removing commas periods etc this is no longer plausible
|
// but with removing commas periods etc this is no longer plausible
|
||||||
const html = this.matchText
|
const html = this.matchText
|
||||||
|
|
||||||
|
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`album-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-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</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>
|
||||||
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || ' ' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
albumMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
album: null,
|
||||||
|
isSelectionMode: false,
|
||||||
|
selected: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
coverSrc() {
|
||||||
|
const config = this.$config || this.$nuxt.$config
|
||||||
|
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
|
return this.store.getters['globals/getLibraryItemCoverSrcById'](this.album.libraryItemId)
|
||||||
|
},
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120
|
||||||
|
return this.width / baseSize
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.album ? this.album.title : ''
|
||||||
|
},
|
||||||
|
artist() {
|
||||||
|
return this.album ? this.album.artist : ''
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(album) {
|
||||||
|
this.album = album
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.album) return
|
||||||
|
// const router = this.$router || this.$nuxt.$router
|
||||||
|
// router.push(`/album/${this.$encode(this.title)}`)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.album)
|
||||||
|
},
|
||||||
|
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.albumMount) {
|
||||||
|
this.setEntity(this.albumMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More Menu Icon -->
|
<!-- More Menu Icon -->
|
||||||
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +190,9 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.mediaType === 'music'
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
const config = this.$config || this.$nuxt.$config
|
const config = this.$config || this.$nuxt.$config
|
||||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
@@ -257,7 +260,7 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
const baseSize = this.squareAspectRatio ? 192 : 120
|
||||||
return this.width / baseSize
|
return this.width / baseSize
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
@@ -273,6 +276,10 @@ export default {
|
|||||||
authorLF() {
|
authorLF() {
|
||||||
return this.mediaMetadata.authorNameLF
|
return this.mediaMetadata.authorNameLF
|
||||||
},
|
},
|
||||||
|
artist() {
|
||||||
|
const artists = this.mediaMetadata.artists || []
|
||||||
|
return artists.join(', ')
|
||||||
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.recentEpisode) return this.recentEpisode.title
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
@@ -282,6 +289,7 @@ export default {
|
|||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
|
if (this.isMusic) return this.artist
|
||||||
if (this.collapsedSeries) return ''
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
@@ -305,6 +313,7 @@ export default {
|
|||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
|
if (this.isMusic) return null
|
||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
@@ -341,7 +350,7 @@ export default {
|
|||||||
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
@@ -366,7 +375,7 @@ export default {
|
|||||||
if (this.isPodcast) return 'Podcast has no episodes'
|
if (this.isPodcast) return 'Podcast has no episodes'
|
||||||
return 'Item has no audio tracks & ebook'
|
return 'Item has no audio tracks & ebook'
|
||||||
}
|
}
|
||||||
var txt = ''
|
let txt = ''
|
||||||
if (this.numMissingParts) {
|
if (this.numMissingParts) {
|
||||||
txt += `${this.numMissingParts} missing parts.`
|
txt += `${this.numMissingParts} missing parts.`
|
||||||
}
|
}
|
||||||
@@ -377,7 +386,7 @@ export default {
|
|||||||
return txt || 'Unknown Error'
|
return txt || 'Unknown Error'
|
||||||
},
|
},
|
||||||
overlayWrapperClasslist() {
|
overlayWrapperClasslist() {
|
||||||
var classes = []
|
const classes = []
|
||||||
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
||||||
else classes.push('bg-opacity-40')
|
else classes.push('bg-opacity-40')
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
@@ -401,6 +410,8 @@ export default {
|
|||||||
return this.store.getters['user/getIsAdminOrUp']
|
return this.store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
moreMenuItems() {
|
moreMenuItems() {
|
||||||
|
if (this.isMusic) return []
|
||||||
|
|
||||||
if (this.recentEpisode) {
|
if (this.recentEpisode) {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -438,7 +449,7 @@ export default {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = []
|
let items = []
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -534,11 +545,11 @@ export default {
|
|||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
var constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView === constants.BookshelfView.DETAIL
|
return this.bookshelfView === constants.BookshelfView.DETAIL
|
||||||
},
|
},
|
||||||
isAuthorBookshelfView() {
|
isAuthorBookshelfView() {
|
||||||
var constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
||||||
},
|
},
|
||||||
titleDisplayBottomOffset() {
|
titleDisplayBottomOffset() {
|
||||||
@@ -548,7 +559,7 @@ export default {
|
|||||||
},
|
},
|
||||||
rssFeed() {
|
rssFeed() {
|
||||||
if (this.booksInSeries) return null
|
if (this.booksInSeries) return null
|
||||||
return this.store.getters['feeds/getFeedForItem'](this.libraryItemId)
|
return this._libraryItem.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -809,7 +820,6 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
console.log('Got library itemn', libraryItem)
|
|
||||||
this.store.commit('showEReader', libraryItem)
|
this.store.commit('showEReader', libraryItem)
|
||||||
},
|
},
|
||||||
selectBtnClick(evt) {
|
selectBtnClick(evt) {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
<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' }">
|
|
||||||
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -72,6 +75,9 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.store.getters['user/getUserCanUpdate']
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
return this.collection ? this.collection.rssFeed : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,9 @@
|
|||||||
<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' }">
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -125,6 +127,9 @@ export default {
|
|||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
const constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
return this.series ? this.series.rssFeed : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -87,8 +87,14 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
seriesItems() {
|
seriesItems() {
|
||||||
return [
|
return [
|
||||||
@@ -174,6 +180,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',
|
||||||
@@ -209,9 +220,33 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
musicItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAll,
|
||||||
|
value: 'all'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelGenre,
|
||||||
|
value: 'genres',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTag,
|
||||||
|
value: 'tags',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonIssues,
|
||||||
|
value: 'issues',
|
||||||
|
sublist: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
if (this.isPodcast) return this.podcastItems
|
if (this.isPodcast) return this.podcastItems
|
||||||
|
if (this.isMusic) return this.musicItems
|
||||||
return this.bookItems
|
return this.bookItems
|
||||||
},
|
},
|
||||||
selectedItemSublist() {
|
selectedItemSublist() {
|
||||||
@@ -263,10 +298,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) => {
|
||||||
|
|||||||
@@ -50,8 +50,14 @@ export default {
|
|||||||
this.$emit('update:descending', val)
|
this.$emit('update:descending', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
return [
|
||||||
@@ -134,10 +140,40 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
musicItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTitle,
|
||||||
|
value: 'media.metadata.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAddedAt,
|
||||||
|
value: 'addedAt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSize,
|
||||||
|
value: 'size'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelDuration,
|
||||||
|
value: 'media.duration'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFileBirthtime,
|
||||||
|
value: 'birthtimeMs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFileModified,
|
||||||
|
value: 'mtimeMs'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
let items = null
|
let items = null
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
items = this.podcastItems
|
items = this.podcastItems
|
||||||
|
} else if (this.isMusic) {
|
||||||
|
items = this.musicItems
|
||||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||||
items = this.seriesItems
|
items = this.seriesItems
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
currentPlaybackRate: 0,
|
currentPlaybackRate: 0,
|
||||||
MIN_SPEED: 0.5,
|
MIN_SPEED: 0.5,
|
||||||
MAX_SPEED: 3,
|
MAX_SPEED: 10,
|
||||||
menuLeft: -92,
|
menuLeft: -92,
|
||||||
arrowLeft: 0
|
arrowLeft: 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export default {
|
|||||||
return `${this.naturalWidth}x${this.naturalHeight}px`
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
},
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
const config = this.$config || this.$nuxt.$config
|
||||||
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div class="flex items-center pt-4 px-2">
|
<div class="flex items-center pt-4 px-2">
|
||||||
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
|
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
|
||||||
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
|
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,55 +31,55 @@
|
|||||||
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
|
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsDownload }}</p>
|
<p id="download-permissions-toggle">{{ $strings.LabelPermissionsDownload }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.download" />
|
<ui-toggle-switch labeledBy="download-permissions-toggle" v-model="newUser.permissions.download" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsUpdate }}</p>
|
<p id="update-permissions-toggle">{{ $strings.LabelPermissionsUpdate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.update" />
|
<ui-toggle-switch labeledBy="update-permissions-toggle" v-model="newUser.permissions.update" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsDelete }}</p>
|
<p id="delete-permissions-toggle">{{ $strings.LabelPermissionsDelete }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.delete" />
|
<ui-toggle-switch labeledBy="delete-permissions-toggle" v-model="newUser.permissions.delete" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsUpload }}</p>
|
<p id="upload-permissions-toggle">{{ $strings.LabelPermissionsUpload }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.upload" />
|
<ui-toggle-switch labeledBy="upload-permissions-toggle" v-model="newUser.permissions.upload" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" />
|
<ui-toggle-switch labeledBy="explicit-content-permissions-toggle" v-model="newUser.permissions.accessExplicitContent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
|
<p id="access-all-libs--permissions-toggle">{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
|
<ui-toggle-switch labeledBy="access-all-libs--permissions-toggle" v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 51" @click="clickClose">
|
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||||
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
||||||
<span class="material-icons text-2xl md:text-4xl">close</span>
|
<span class="material-icons text-2xl md:text-4xl">close</span>
|
||||||
</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>
|
||||||
@@ -109,7 +109,8 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error(this.$strings.ToastAuthorUpdateFailed)
|
const errorMsg = error.response ? error.response.data : null
|
||||||
|
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -125,8 +126,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async removeCover() {
|
async removeCover() {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
imagePath: null,
|
imagePath: null
|
||||||
relImagePath: null
|
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
@@ -161,8 +161,7 @@ export default {
|
|||||||
if (response.author.imagePath) {
|
if (response.author.imagePath) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
this.$store.commit('globals/showEditAuthorModal', response.author)
|
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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -303,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
|
||||||
@@ -376,6 +396,12 @@ export default {
|
|||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-provider') || 'google'
|
||||||
|
|
||||||
|
// Prefer using ASIN if set and using audible provider
|
||||||
|
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
|
||||||
|
this.searchTitle = this.libraryItem.media.metadata.asin
|
||||||
|
this.searchAuthor = ''
|
||||||
|
}
|
||||||
|
|
||||||
if (this.searchTitle) {
|
if (this.searchTitle) {
|
||||||
this.submitSearch()
|
this.submitSearch()
|
||||||
}
|
}
|
||||||
@@ -465,11 +491,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 +512,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 +531,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
clearSelectedMatch() {
|
clearSelectedMatch() {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<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>
|
||||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||||
<ui-text-input-with-label v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
<ui-text-input-with-label ref="nameInput" v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||||
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
||||||
@@ -16,16 +16,16 @@
|
|||||||
</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 ref="folderInput" v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||||
<span v-show="folders.length > 1" class="material-icons text-2xl 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>
|
||||||
<ui-editable-text v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||||
@@ -67,6 +67,10 @@ export default {
|
|||||||
value: 'podcast',
|
value: 'podcast',
|
||||||
text: this.$strings.LabelPodcasts
|
text: this.$strings.LabelPodcasts
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// value: 'music',
|
||||||
|
// text: 'Music'
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
folderPaths() {
|
folderPaths() {
|
||||||
@@ -78,6 +82,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkBlurExpressionInput() {
|
||||||
|
if (this.$refs.nameInput) {
|
||||||
|
this.$refs.nameInput.blur()
|
||||||
|
}
|
||||||
|
if (this.$refs.folderInput && this.$refs.folderInput.length) {
|
||||||
|
this.$refs.folderInput.forEach((input) => {
|
||||||
|
if (input.blur) input.blur()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.$refs.newFolderInput) {
|
||||||
|
this.$refs.newFolderInput.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
browseForFolder() {
|
browseForFolder() {
|
||||||
this.showDirectoryPicker = true
|
this.showDirectoryPicker = true
|
||||||
},
|
},
|
||||||
@@ -140,3 +157,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">
|
||||||
@@ -144,8 +144,6 @@ export default {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.validate()) return
|
|
||||||
|
|
||||||
// If custom expression input is focused then unfocus it instead of submitting
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
||||||
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
||||||
@@ -153,6 +151,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.validate()) return
|
||||||
|
|
||||||
if (this.library) {
|
if (this.library) {
|
||||||
this.submitUpdateLibrary()
|
this.submitUpdateLibrary()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+33
-44
@@ -6,13 +6,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div v-if="currentFeedUrl" class="w-full">
|
<div v-if="currentFeed" class="w-full">
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeedUrl" readonly />
|
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeedUrl)">content_copy</span>
|
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
<ui-btn v-if="currentFeed" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
||||||
<ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
<ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,19 +37,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
value: Boolean,
|
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
feedUrl: String
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newFeedSlug: null,
|
newFeedSlug: null,
|
||||||
currentFeedUrl: null
|
currentFeed: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -65,23 +57,29 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
return this.value
|
return this.$store.state.globals.showRSSFeedOpenCloseModal
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$store.commit('globals/setShowRSSFeedOpenCloseModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
rssFeedEntity() {
|
||||||
return this.libraryItem.id
|
return this.$store.state.globals.rssFeedEntity || {}
|
||||||
},
|
},
|
||||||
media() {
|
entityId() {
|
||||||
return this.libraryItem.media || {}
|
return this.rssFeedEntity.id
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
entityType() {
|
||||||
return this.media.metadata || {}
|
return this.rssFeedEntity.type
|
||||||
|
},
|
||||||
|
entityFeed() {
|
||||||
|
return this.rssFeedEntity.feed
|
||||||
|
},
|
||||||
|
hasEpisodesWithoutPubDate() {
|
||||||
|
return !!this.rssFeedEntity.hasEpisodesWithoutPubDate
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title
|
return this.rssFeedEntity.name
|
||||||
},
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
@@ -91,12 +89,6 @@ export default {
|
|||||||
},
|
},
|
||||||
isHttp() {
|
isHttp() {
|
||||||
return window.origin.startsWith('http://')
|
return window.origin.startsWith('http://')
|
||||||
},
|
|
||||||
episodes() {
|
|
||||||
return this.media.episodes || []
|
|
||||||
},
|
|
||||||
hasEpisodesWithoutPubDate() {
|
|
||||||
return this.episodes.some((ep) => !ep.pubDate)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -106,7 +98,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||||
if (this.newFeedSlug !== sanitized) {
|
if (this.newFeedSlug !== sanitized) {
|
||||||
this.newFeedSlug = sanitized
|
this.newFeedSlug = sanitized
|
||||||
this.$toast.warning('Slug had to be modified - Run again')
|
this.$toast.warning('Slug had to be modified - Run again')
|
||||||
@@ -121,19 +113,15 @@ export default {
|
|||||||
|
|
||||||
console.log('Payload', payload)
|
console.log('Payload', payload)
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/${this.libraryItemId}/open-feed`, payload)
|
.$post(`/api/feeds/${this.entityType}/${this.entityId}/open`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.success) {
|
console.log('Opened RSS Feed', data)
|
||||||
console.log('Opened RSS Feed', data)
|
this.currentFeed = data.feed
|
||||||
this.currentFeedUrl = data.feedUrl
|
|
||||||
} else {
|
|
||||||
const errorMsg = data.error || 'Unknown error'
|
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to open RSS Feed', error)
|
console.error('Failed to open RSS Feed', error)
|
||||||
this.$toast.error()
|
const errorMsg = error.response ? error.response.data : null
|
||||||
|
this.$toast.error(errorMsg || 'Failed to open RSS Feed')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
copyToClipboard(str) {
|
copyToClipboard(str) {
|
||||||
@@ -142,22 +130,23 @@ export default {
|
|||||||
closeFeed() {
|
closeFeed() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/${this.libraryItem.id}/close-feed`)
|
.$post(`/api/feeds/${this.currentFeed.id}/close`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
this.processing = false
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to close RSS feed', error)
|
console.error('Failed to close RSS feed', error)
|
||||||
this.processing = false
|
|
||||||
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
if (!this.libraryItem) return
|
if (!this.entityId) return
|
||||||
this.newFeedSlug = this.libraryItem.id
|
this.newFeedSlug = this.entityId
|
||||||
this.currentFeedUrl = this.feedUrl
|
this.currentFeed = this.entityFeed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
@@ -234,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() {
|
||||||
@@ -311,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)
|
||||||
@@ -345,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>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<div id="viewer" class="border border-gray-100 bg-white text-black shadow-md h-screen overflow-y-auto p-4" v-html="pageHtml"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bookInfo: {},
|
||||||
|
page: 0,
|
||||||
|
numPages: 0,
|
||||||
|
pageHtml: '',
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
|
},
|
||||||
|
hasPrev() {
|
||||||
|
return this.page > 0
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
return this.page < this.numPages - 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prev() {
|
||||||
|
if (!this.hasPrev) return
|
||||||
|
this.page--
|
||||||
|
this.loadPage()
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (!this.hasNext) return
|
||||||
|
this.page++
|
||||||
|
this.loadPage()
|
||||||
|
},
|
||||||
|
keyUp() {
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
this.prev()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadPage() {
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/ebooks/${this.libraryItemId}/page/${this.page}?dev=${this.$isDev ? 1 : 0}`)
|
||||||
|
.then((html) => {
|
||||||
|
this.pageHtml = html
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load page', error)
|
||||||
|
this.$toast.error('Failed to load page')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadInfo() {
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/ebooks/${this.libraryItemId}/info?dev=${this.$isDev ? 1 : 0}`)
|
||||||
|
.then((bookInfo) => {
|
||||||
|
this.bookInfo = bookInfo
|
||||||
|
this.numPages = bookInfo.pages
|
||||||
|
this.page = 0
|
||||||
|
this.loadPage()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load page', error)
|
||||||
|
this.$toast.error('Failed to load info')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initEpub() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
this.loadInfo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initEpub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
||||||
|
|
||||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentName() {
|
componentName() {
|
||||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2'
|
||||||
|
else if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
@@ -92,13 +93,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']
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
<p class="text-sm font-book">{{ dayObj.dayOfWeekAbbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +108,7 @@ export default {
|
|||||||
var _date = this.$addDaysToToday(i * -1)
|
var _date = this.$addDaysToToday(i * -1)
|
||||||
days.push({
|
days.push({
|
||||||
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
||||||
|
dayOfWeekAbbr: this.$formatJsDate(_date, 'EEE'),
|
||||||
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default {
|
|||||||
dayLabels() {
|
dayLabels() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Mon',
|
label: this.$formatJsDate(new Date(2023, 0, 2), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13}px)`,
|
transform: `translate(${-25}px, ${13}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -76,7 +76,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Wed',
|
label: this.$formatJsDate(new Date(2023, 0, 4), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 3}px)`,
|
transform: `translate(${-25}px, ${13 * 3}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -84,7 +84,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fri',
|
label: this.$formatJsDate(new Date(2023, 0, 6), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 5}px)`,
|
transform: `translate(${-25}px, ${13 * 5}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-left">
|
||||||
<!-- Dont show edit for non-root users -->
|
<!-- Dont show edit for non-root users -->
|
||||||
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||||
<span class="material-icons text-base">edit</span>
|
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||||
<span class="material-icons text-base">delete</span>
|
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -109,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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="librariesTable">
|
<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">
|
||||||
@@ -82,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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
<div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
||||||
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
||||||
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
||||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||||
<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>
|
||||||
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
<p class="text-base md:text-xl font-book pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">{{ $strings.ButtonScan }}</ui-btn>
|
|
||||||
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">{{ $strings.ButtonForceReScan }}</ui-btn>
|
|
||||||
|
|
||||||
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">{{ $strings.ButtonMatchBooks }}</ui-btn>
|
<!-- Desktop context menu icon -->
|
||||||
|
<ui-context-menu-dropdown v-if="!libraryScan && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" />
|
||||||
|
|
||||||
<span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
<!-- Mobile context menu icon -->
|
||||||
<span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>
|
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
|
||||||
<span v-if="!libraryScan" class="!block md:!hidden material-icons text-xl text-gray-300 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
|
||||||
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-2xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
|
|
||||||
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-2 md:ml-4">reorder</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
<!-- For mobile -->
|
||||||
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="contextMenuItems" @action="contextMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,34 +60,45 @@ export default {
|
|||||||
menuTitle() {
|
menuTitle() {
|
||||||
return this.library.name
|
return this.library.name
|
||||||
},
|
},
|
||||||
mobileMenuItems() {
|
contextMenuItems() {
|
||||||
const items = [
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonEdit,
|
||||||
|
action: 'edit',
|
||||||
|
value: 'edit'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonScan,
|
text: this.$strings.ButtonScan,
|
||||||
|
action: 'scan',
|
||||||
value: 'scan'
|
value: 'scan'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonForceReScan,
|
text: this.$strings.ButtonForceReScan,
|
||||||
|
action: 'force-scan',
|
||||||
value: 'force-scan'
|
value: 'force-scan'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.isBookLibrary) {
|
if (this.isBookLibrary) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonMatchBooks,
|
text: this.$strings.ButtonMatchBooks,
|
||||||
|
action: 'match-books',
|
||||||
value: 'match-books'
|
value: 'match-books'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonDelete,
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete',
|
||||||
value: 'delete'
|
value: 'delete'
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mobileMenuAction(action) {
|
contextMenuAction(action) {
|
||||||
this.showMobileMenu = false
|
this.showMobileMenu = false
|
||||||
if (action === 'scan') {
|
if (action === 'edit') {
|
||||||
|
this.editClick()
|
||||||
|
} else if (action === 'scan') {
|
||||||
this.scan()
|
this.scan()
|
||||||
} else if (action === 'force-scan') {
|
} else if (action === 'force-scan') {
|
||||||
this.forceScan()
|
this.forceScan()
|
||||||
@@ -130,37 +138,52 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
forceScan() {
|
forceScan() {
|
||||||
if (confirm(this.$strings.MessageConfirmForceReScan)) {
|
const payload = {
|
||||||
this.$store
|
message: this.$strings.MessageConfirmForceReScan,
|
||||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
callback: (confirmed) => {
|
||||||
.then(() => {
|
if (confirmed) {
|
||||||
this.$toast.success(this.$strings.ToastLibraryScanStarted)
|
this.$store
|
||||||
})
|
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
||||||
.catch((error) => {
|
.then(() => {
|
||||||
console.error('Failed to start scan', error)
|
this.$toast.success(this.$strings.ToastLibraryScanStarted)
|
||||||
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to start scan', error)
|
||||||
|
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
deleteClick() {
|
deleteClick() {
|
||||||
if (confirm(this.$getString('MessageConfirmDeleteLibrary', [this.library.name]))) {
|
const payload = {
|
||||||
this.isDeleting = true
|
message: this.$getString('MessageConfirmDeleteLibrary', [this.library.name]),
|
||||||
this.$axios
|
callback: (confirmed) => {
|
||||||
.$delete(`/api/libraries/${this.library.id}`)
|
if (confirmed) {
|
||||||
.then((data) => {
|
this.isDeleting = true
|
||||||
this.isDeleting = false
|
this.$axios
|
||||||
if (data.error) {
|
.$delete(`/api/libraries/${this.library.id}`)
|
||||||
this.$toast.error(data.error)
|
.then((data) => {
|
||||||
} else {
|
if (data.error) {
|
||||||
this.$toast.success(this.$strings.ToastLibraryDeleteSuccess)
|
this.$toast.error(data.error)
|
||||||
}
|
} else {
|
||||||
})
|
this.$toast.success(this.$strings.ToastLibraryDeleteSuccess)
|
||||||
.catch((error) => {
|
}
|
||||||
console.error('Failed to delete library', error)
|
})
|
||||||
this.$toast.error(this.$strings.ToastLibraryDeleteFailed)
|
.catch((error) => {
|
||||||
this.isDeleting = false
|
console.error('Failed to delete library', error)
|
||||||
})
|
this.$toast.error(this.$strings.ToastLibraryDeleteFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isDeleting = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full py-6">
|
<div class="w-full py-6">
|
||||||
|
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
|
<p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<template v-if="isSelectionMode">
|
<template v-if="isSelectionMode">
|
||||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||||
@@ -11,8 +12,10 @@
|
|||||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" />
|
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 sm:ml-4" />
|
||||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||||
|
<div class="flex-grow md:hidden" />
|
||||||
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
</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>
|
||||||
@@ -42,15 +45,27 @@ export default {
|
|||||||
showPodcastRemoveModal: false,
|
showPodcastRemoveModal: false,
|
||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false
|
processing: false,
|
||||||
|
quickMatchingEpisodes: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
libraryItem() {
|
libraryItem: {
|
||||||
this.init()
|
handler() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
contextMenuItems() {
|
||||||
|
if (!this.userIsAdminOrUp) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Quick match all episodes',
|
||||||
|
action: 'quick-match-episodes'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
sortItems() {
|
sortItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -94,8 +109,8 @@ export default {
|
|||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.selectedEpisodes.length > 0
|
return this.selectedEpisodes.length > 0
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
@@ -131,6 +146,44 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'quick-match-episodes') {
|
||||||
|
if (this.quickMatchingEpisodes) return
|
||||||
|
|
||||||
|
this.quickMatchAllEpisodes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quickMatchAllEpisodes() {
|
||||||
|
if (!this.mediaMetadata.feedUrl) {
|
||||||
|
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.quickMatchingEpisodes = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.numEpisodesUpdated) {
|
||||||
|
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
||||||
|
} else {
|
||||||
|
this.$toast.info('No changes were made')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to request match episodes', error)
|
||||||
|
this.$toast.error('Failed to match episodes')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.quickMatchingEpisodes = false
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
addToPlaylist(episode) {
|
addToPlaylist(episode) {
|
||||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
||||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<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="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="material-icons" :class="iconClass">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: () => []
|
||||||
|
},
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||||
<span v-if="selectedSubtext">: </span>
|
<span v-if="selectedSubtext">: </span>
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||||
<span v-if="item.subtext">: </span>
|
<span v-if="item.subtext">: </span>
|
||||||
@@ -91,6 +91,13 @@ export default {
|
|||||||
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
||||||
|
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
longLabel() {
|
||||||
|
let result = ''
|
||||||
|
if (this.label) result += this.label + ': '
|
||||||
|
if (this.selectedText) result += this.selectedText
|
||||||
|
if (this.selectedSubtext) result += ' ' + this.selectedSubtext
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name" @click.stop.prevent="clickShowMenu">
|
||||||
<div class="flex items-center justify-center sm:justify-start">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="library in librariesFiltered">
|
<template v-for="library in librariesFiltered">
|
||||||
<li :key="library.id" class="text-gray-400 hover:text-white select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="selectLibrary(library)">
|
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul ref="menu" v-show="showMenu" class="absolute z-50 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
}, 50)
|
}, 50)
|
||||||
},
|
},
|
||||||
recalcMenuPos() {
|
recalcMenuPos() {
|
||||||
if (!this.menu) return
|
if (!this.menu || !this.$refs.inputWrapper) return
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
if (boundingBox.y > window.innerHeight - 8) {
|
if (boundingBox.y > window.innerHeight - 8) {
|
||||||
// Input is off the page
|
// Input is off the page
|
||||||
@@ -135,7 +135,7 @@ export default {
|
|||||||
this.menu.style.width = boundingBox.width + 'px'
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
},
|
},
|
||||||
unmountMountMenu() {
|
unmountMountMenu() {
|
||||||
if (!this.$refs.menu) return
|
if (!this.$refs.menu || !this.$refs.inputWrapper) return
|
||||||
this.menu = this.$refs.menu
|
this.menu = this.$refs.menu
|
||||||
|
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul ref="menu" v-show="showMenu" class="absolute z-50 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -68,14 +68,14 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
recalcMenuPos() {
|
recalcMenuPos() {
|
||||||
if (!this.menu) return
|
if (!this.menu || !this.$refs.inputWrapper) return
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
this.menu.style.top = boundingBox.y + boundingBox.height - 4 + 'px'
|
this.menu.style.top = boundingBox.y + boundingBox.height - 4 + 'px'
|
||||||
this.menu.style.left = boundingBox.x + 'px'
|
this.menu.style.left = boundingBox.x + 'px'
|
||||||
this.menu.style.width = boundingBox.width + 'px'
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
},
|
},
|
||||||
unmountMountMenu() {
|
unmountMountMenu() {
|
||||||
if (!this.$refs.menu) return
|
if (!this.$refs.menu || !this.$refs.inputWrapper) return
|
||||||
this.menu = this.$refs.menu
|
this.menu = this.$refs.menu
|
||||||
|
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul ref="menu" v-show="showMenu" class="absolute z-50 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -113,10 +113,14 @@ 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
|
||||||
},
|
},
|
||||||
@@ -136,7 +140,7 @@ export default {
|
|||||||
}, 50)
|
}, 50)
|
||||||
},
|
},
|
||||||
recalcMenuPos() {
|
recalcMenuPos() {
|
||||||
if (!this.menu) return
|
if (!this.menu || !this.$refs.inputWrapper) return
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
if (boundingBox.y > window.innerHeight - 8) {
|
if (boundingBox.y > window.innerHeight - 8) {
|
||||||
// Input is off the page
|
// Input is off the page
|
||||||
@@ -154,7 +158,7 @@ export default {
|
|||||||
this.menu.style.width = boundingBox.width + 'px'
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
},
|
},
|
||||||
unmountMountMenu() {
|
unmountMountMenu() {
|
||||||
if (!this.$refs.menu) return
|
if (!this.$refs.menu || !this.$refs.inputWrapper) return
|
||||||
this.menu = this.$refs.menu
|
this.menu = this.$refs.menu
|
||||||
|
|
||||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
@@ -200,15 +204,21 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.$refs.input) this.$refs.input.focus()
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
|
||||||
var newSelected = null
|
let newSelected = null
|
||||||
if (this.getIsSelected(item.id)) {
|
if (this.getIsSelected(item.id)) {
|
||||||
newSelected = this.selected.filter((s) => s.id !== item.id)
|
newSelected = this.selected.filter((s) => s.id !== item.id)
|
||||||
this.$emit('removedItem', item.id)
|
this.$emit('removedItem', item.id)
|
||||||
} else {
|
} else {
|
||||||
newSelected = this.selected.concat([item])
|
newSelected = this.selected.concat([
|
||||||
|
{
|
||||||
|
id: item.id,
|
||||||
|
name: item.name
|
||||||
|
}
|
||||||
|
])
|
||||||
}
|
}
|
||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
|
|
||||||
this.$emit('input', newSelected)
|
this.$emit('input', newSelected)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalcMenuPos()
|
this.recalcMenuPos()
|
||||||
@@ -242,10 +252,11 @@ export default {
|
|||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
|
|
||||||
var cleaned = this.textInput.trim()
|
const cleaned = this.textInput.trim()
|
||||||
var matchesItem = this.items.find((i) => {
|
const matchesItem = this.items.find((i) => {
|
||||||
return i === cleaned
|
return i.name === cleaned
|
||||||
})
|
})
|
||||||
|
|
||||||
if (matchesItem) {
|
if (matchesItem) {
|
||||||
this.clickedOption(null, matchesItem)
|
this.clickedOption(null, matchesItem)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul ref="menu" v-show="isFocused && currentSearch" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul ref="menu" v-show="isFocused && currentSearch" class="absolute z-60 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<input ref="input" v-model="inputValue" :type="actualType" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
<input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||||
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,7 +31,8 @@ export default {
|
|||||||
},
|
},
|
||||||
noSpinner: Boolean,
|
noSpinner: Boolean,
|
||||||
textCenter: Boolean,
|
textCenter: Boolean,
|
||||||
clearable: Boolean
|
clearable: Boolean,
|
||||||
|
inputId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<slot>
|
<slot>
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
|
||||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
|
||||||
</p>
|
>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
<ui-text-input :placeholder="label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :class="className" @click="clickToggle">
|
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||||
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'primary'
|
default: 'primary'
|
||||||
},
|
},
|
||||||
disabled: Boolean
|
disabled: Boolean,
|
||||||
|
labeledBy: String
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleValue: {
|
toggleValue: {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,11 +210,13 @@ export default {
|
|||||||
// array of objects with id key
|
// array of objects with id key
|
||||||
if (array1.length !== array2.length) return false
|
if (array1.length !== array2.length) return false
|
||||||
|
|
||||||
for (var item of array1) {
|
for (let i = 0; i < array1.length; i++) {
|
||||||
var matchingItem = array2.find((a) => a.id === item.id)
|
const item1 = array1[i]
|
||||||
if (!matchingItem) return false
|
const item2 = array2[i]
|
||||||
for (var key in item) {
|
if (!item1 || !item2) return false
|
||||||
if (item[key] !== matchingItem[key]) {
|
|
||||||
|
for (const key in item1) {
|
||||||
|
if (item1[key] !== item2[key]) {
|
||||||
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,31 +137,31 @@ export default {
|
|||||||
weekdays() {
|
weekdays() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySunday,
|
text: this.$formatJsDate(new Date(2023, 0, 1), 'EEEE'),
|
||||||
value: 0
|
value: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayMonday,
|
text: this.$formatJsDate(new Date(2023, 0, 2), 'EEEE'),
|
||||||
value: 1
|
value: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayTuesday,
|
text: this.$formatJsDate(new Date(2023, 0, 3), 'EEEE'),
|
||||||
value: 2
|
value: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayWednesday,
|
text: this.$formatJsDate(new Date(2023, 0, 4), 'EEEE'),
|
||||||
value: 3
|
value: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayThursday,
|
text: this.$formatJsDate(new Date(2023, 0, 5), 'EEEE'),
|
||||||
value: 4
|
value: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayFriday,
|
text: this.$formatJsDate(new Date(2023, 0, 6), 'EEEE'),
|
||||||
value: 5
|
value: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySaturday,
|
text: this.$formatJsDate(new Date(2023, 0, 7), 'EEEE'),
|
||||||
value: 6
|
value: 6
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export default {
|
|||||||
..._series
|
..._series
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Selected series', this.selectedSeries)
|
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
addNewSeries() {
|
addNewSeries() {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<modals-podcast-view-episode />
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
<modals-batch-quick-match-model />
|
<modals-batch-quick-match-model />
|
||||||
|
<modals-rssfeed-open-close-modal />
|
||||||
<prompt-confirm />
|
<prompt-confirm />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
@@ -42,9 +43,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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,7 +281,6 @@ 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) {
|
||||||
@@ -331,12 +330,6 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||||
},
|
},
|
||||||
rssFeedOpen(data) {
|
|
||||||
this.$store.commit('feeds/addFeed', data)
|
|
||||||
},
|
|
||||||
rssFeedClosed(data) {
|
|
||||||
this.$store.commit('feeds/removeFeed', data)
|
|
||||||
},
|
|
||||||
backupApplied() {
|
backupApplied() {
|
||||||
// Force refresh
|
// Force refresh
|
||||||
location.reload()
|
location.reload()
|
||||||
@@ -426,10 +419,6 @@ export default {
|
|||||||
this.socket.on('task_started', this.taskStarted)
|
this.socket.on('task_started', this.taskStarted)
|
||||||
this.socket.on('task_finished', this.taskFinished)
|
this.socket.on('task_finished', this.taskFinished)
|
||||||
|
|
||||||
// Feed Listeners
|
|
||||||
this.socket.on('rss_feed_open', this.rssFeedOpen)
|
|
||||||
this.socket.on('rss_feed_closed', this.rssFeedClosed)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -504,9 +493,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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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'
|
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||||
|
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -17,6 +18,7 @@ export default {
|
|||||||
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)
|
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||||
|
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
async mountEntityCard(index) {
|
async mountEntityCard(index) {
|
||||||
@@ -28,11 +30,11 @@ export default {
|
|||||||
}
|
}
|
||||||
this.entityIndexesMounted.push(index)
|
this.entityIndexesMounted.push(index)
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
var bookComponent = this.entityComponentRefs[index]
|
const bookComponent = this.entityComponentRefs[index]
|
||||||
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
|
||||||
@@ -43,13 +45,13 @@ export default {
|
|||||||
bookComponent.isHovering = false
|
bookComponent.isHovering = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var shelfOffsetY = 16
|
const shelfOffsetY = 16
|
||||||
var row = index % this.entitiesPerShelf
|
const row = index % this.entitiesPerShelf
|
||||||
var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||||
|
|
||||||
var ComponentClass = this.getComponentClass()
|
const ComponentClass = this.getComponentClass()
|
||||||
|
|
||||||
var props = {
|
const props = {
|
||||||
index,
|
index,
|
||||||
width: this.entityWidth,
|
width: this.entityWidth,
|
||||||
height: this.entityHeight,
|
height: this.entityHeight,
|
||||||
@@ -58,15 +60,15 @@ export default {
|
|||||||
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'items') {
|
||||||
props.filterBy = this.filterBy
|
props.filterBy = this.filterBy
|
||||||
props.orderBy = this.orderBy
|
props.orderBy = this.orderBy
|
||||||
} else if (this.entityName === 'series') {
|
} else if (this.entityName === 'series') {
|
||||||
props.orderBy = this.seriesSortBy
|
props.orderBy = this.seriesSortBy
|
||||||
}
|
}
|
||||||
|
|
||||||
var _this = this
|
const _this = this
|
||||||
var instance = new ComponentClass({
|
const instance = new ComponentClass({
|
||||||
propsData: props,
|
propsData: props,
|
||||||
created() {
|
created() {
|
||||||
this.$on('edit', (entity) => {
|
this.$on('edit', (entity) => {
|
||||||
@@ -89,7 +91,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,15 +110,21 @@ module.exports = {
|
|||||||
short_name: 'Audiobookshelf',
|
short_name: 'Audiobookshelf',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#373838',
|
background_color: '#373838',
|
||||||
start_url: '',
|
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
||||||
sizes: "any"
|
sizes: "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png',
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "64x64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
|
offline: false,
|
||||||
|
cacheAssets: false,
|
||||||
preCaching: [],
|
preCaching: [],
|
||||||
runtimeCaching: []
|
runtimeCaching: []
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.6",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.6",
|
"version": "2.2.13",
|
||||||
"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.6",
|
"version": "2.2.13",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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,28 +45,28 @@
|
|||||||
</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">
|
||||||
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
||||||
<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 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)">
|
||||||
@@ -96,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>
|
||||||
@@ -177,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>
|
||||||
@@ -190,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']) {
|
||||||
@@ -232,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: {
|
||||||
@@ -274,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
|
||||||
@@ -304,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,
|
||||||
@@ -319,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)
|
||||||
@@ -353,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
|
||||||
|
|
||||||
@@ -369,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'
|
||||||
@@ -413,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,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) {
|
||||||
@@ -513,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()
|
||||||
|
|||||||
@@ -62,19 +62,42 @@
|
|||||||
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div v-if="selectedTool === 'embed'" class="w-full flex justify-end items-center mb-4">
|
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||||
|
<ui-checkbox v-if="!isFinished" v-model="shouldBackupAudioFiles" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full flex justify-end items-center mb-4">
|
<div v-else class="w-full flex items-center mb-4">
|
||||||
|
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
||||||
|
<span class="material-icons text-xl">{{ showEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isTaskFinished && processing" color="error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">{{ $strings.ButtonCancelEncode }}</ui-btn>
|
<ui-btn v-if="!isTaskFinished && processing" color="error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">{{ $strings.ButtonCancelEncode }}</ui-btn>
|
||||||
<ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">{{ $strings.ButtonStartM4BEncode }}</ui-btn>
|
<ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">{{ $strings.ButtonStartM4BEncode }}</ui-btn>
|
||||||
<p v-else-if="taskFailed" class="text-error text-lg font-semibold">{{ $strings.MessageM4BFailed }} {{ taskError }}</p>
|
<p v-else-if="taskFailed" class="text-error text-lg font-semibold">{{ $strings.MessageM4BFailed }} {{ taskError }}</p>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isM4BTool" class="overflow-hidden">
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||||
|
<div class="flex flex-wrap -mx-2">
|
||||||
|
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 64k)'" class="m-2 max-w-40" />
|
||||||
|
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="'Audio Channels (1 or 2)'" class="m-2 max-w-40" />
|
||||||
|
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-warning">Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.</p>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div v-if="selectedTool === 'embed'" class="flex items-start mb-2">
|
<div v-if="isEmbedTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p>
|
<p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,21 +108,21 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start mb-2">
|
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">
|
<p class="text-gray-200 ml-2">
|
||||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedTool === 'embed' && audioFiles.length > 1" class="flex items-start mb-2">
|
<div v-if="isEmbedTool && audioFiles.length > 1" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p>
|
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedTool === 'm4b'" class="flex items-start mb-2">
|
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p>
|
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedTool === 'm4b'" class="flex items-start mb-2">
|
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">If you have the watcher disabled you will need to re-scan this audiobook afterwards.</p>
|
<p class="text-gray-200 ml-2">If you have the watcher disabled you will need to re-scan this audiobook afterwards.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +175,7 @@ export default {
|
|||||||
if (!store.getters['user/getIsAdminOrUp']) {
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
const libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -180,7 +203,14 @@ export default {
|
|||||||
isFinished: false,
|
isFinished: false,
|
||||||
toneObject: null,
|
toneObject: null,
|
||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false
|
isCancelingEncode: false,
|
||||||
|
showEncodeOptions: false,
|
||||||
|
shouldBackupAudioFiles: true,
|
||||||
|
encodingOptions: {
|
||||||
|
bitrate: '64k',
|
||||||
|
channels: '2',
|
||||||
|
codec: 'aac'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -193,6 +223,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isEmbedTool() {
|
||||||
|
return this.selectedTool === 'embed'
|
||||||
|
},
|
||||||
|
isM4BTool() {
|
||||||
|
return this.selectedTool === 'm4b'
|
||||||
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
},
|
},
|
||||||
@@ -244,6 +280,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleBackupAudioFiles(val) {
|
||||||
|
localStorage.setItem('embedMetadataShouldBackup', val ? 1 : 0)
|
||||||
|
},
|
||||||
cancelEncodeClick() {
|
cancelEncodeClick() {
|
||||||
this.isCancelingEncode = true
|
this.isCancelingEncode = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -260,9 +299,23 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
encodeM4bClick() {
|
encodeM4bClick() {
|
||||||
|
if (this.$refs.bitrateInput) this.$refs.bitrateInput.blur()
|
||||||
|
if (this.$refs.channelsInput) this.$refs.channelsInput.blur()
|
||||||
|
if (this.$refs.codecInput) this.$refs.codecInput.blur()
|
||||||
|
|
||||||
|
let queryStr = ''
|
||||||
|
if (this.showEncodeOptions) {
|
||||||
|
const options = []
|
||||||
|
if (this.encodingOptions.bitrate) options.push(`bitrate=${this.encodingOptions.bitrate}`)
|
||||||
|
if (this.encodingOptions.channels) options.push(`channels=${this.encodingOptions.channels}`)
|
||||||
|
if (this.encodingOptions.codec) options.push(`codec=${this.encodingOptions.codec}`)
|
||||||
|
if (options.length) {
|
||||||
|
queryStr = `?${options.join('&')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b${queryStr}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Ab m4b merge started')
|
console.log('Ab m4b merge started')
|
||||||
})
|
})
|
||||||
@@ -287,7 +340,7 @@ export default {
|
|||||||
updateAudioFileMetadata() {
|
updateAudioFileMetadata() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?tone=1`)
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?backup=${this.shouldBackupAudioFiles ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio metadata encode started')
|
console.log('Audio metadata encode started')
|
||||||
})
|
})
|
||||||
@@ -305,9 +358,14 @@ export default {
|
|||||||
console.log('audio metadata finished', data)
|
console.log('audio metadata finished', data)
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.isFinished = true
|
|
||||||
this.audiofilesEncoding = {}
|
this.audiofilesEncoding = {}
|
||||||
this.$toast.success('Audio file metadata updated')
|
|
||||||
|
if (data.failed) {
|
||||||
|
this.$toast.error(data.error)
|
||||||
|
} else {
|
||||||
|
this.isFinished = true
|
||||||
|
this.$toast.success('Audio file metadata updated')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
audiofileMetadataStarted(data) {
|
audiofileMetadataStarted(data) {
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
@@ -333,6 +391,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.task) this.taskUpdated(this.task)
|
if (this.task) this.taskUpdated(this.task)
|
||||||
|
|
||||||
|
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
||||||
|
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
||||||
},
|
},
|
||||||
fetchToneObject() {
|
fetchToneObject() {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -4,12 +4,23 @@
|
|||||||
<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 text-2xl">{{ 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')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,9 +19,16 @@
|
|||||||
{{ 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" />
|
<!-- RSS feed -->
|
||||||
|
<ui-tooltip v-if="rssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="showRSSFeedModal" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
<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-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 +39,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>
|
||||||
@@ -44,7 +51,7 @@ export default {
|
|||||||
if (!store.state.user.user) {
|
if (!store.state.user.user) {
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
}
|
}
|
||||||
var collection = await app.$axios.$get(`/api/collections/${params.id}`).catch((error) => {
|
const collection = await app.$axios.$get(`/api/collections/${params.id}?include=rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -59,12 +66,13 @@ export default {
|
|||||||
|
|
||||||
store.commit('libraries/addUpdateCollection', collection)
|
store.commit('libraries/addUpdateCollection', collection)
|
||||||
return {
|
return {
|
||||||
collectionId: collection.id
|
collectionId: collection.id,
|
||||||
|
rssFeed: collection.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingRemove: false
|
processing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -97,20 +105,79 @@ export default {
|
|||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return this.playableBooks.length
|
return this.playableBooks.length
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
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.userIsAdminOrUp || this.rssFeed) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelOpenRSSFeed,
|
||||||
|
action: 'open-rss-feed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.userCanDelete) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showRSSFeedModal() {
|
||||||
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.collectionId,
|
||||||
|
name: this.collectionName,
|
||||||
|
type: 'collection',
|
||||||
|
feed: this.rssFeed
|
||||||
|
})
|
||||||
|
},
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
this.removeClick()
|
||||||
|
} else if (action === 'create-playlist') {
|
||||||
|
this.createPlaylistFromCollection()
|
||||||
|
} else if (action === 'open-rss-feed') {
|
||||||
|
this.showRSSFeedModal()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||||
this.processingRemove = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/collections/${this.collection.id}`)
|
.$delete(`/api/collections/${this.collection.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -121,7 +188,7 @@ export default {
|
|||||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingRemove = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,9 +231,27 @@ export default {
|
|||||||
queueItems
|
queueItems
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rssFeedOpen(data) {
|
||||||
|
if (data.entityId === this.collectionId) {
|
||||||
|
console.log('RSS Feed Opened', data)
|
||||||
|
this.rssFeed = data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rssFeedClosed(data) {
|
||||||
|
if (data.entityId === this.collectionId) {
|
||||||
|
console.log('RSS Feed Closed', data)
|
||||||
|
this.rssFeed = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
beforeDestroy() {}
|
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div id="page-wrapper" class="page p-2 md:p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-2 md:p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<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="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore">
|
||||||
<span class="material-icons text-2xl cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
|
||||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
<p class="pl-3 capitalize">{{ $strings.HeaderSettings }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
</div>
|
</div>
|
||||||
@@ -35,8 +35,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() {
|
isMobilePortrait() {
|
||||||
return this.$store.state.globals.isMobile
|
return this.$store.state.globals.isMobilePortrait
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@@ -54,13 +54,14 @@ export default {
|
|||||||
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
}
|
}
|
||||||
return this.$strings.HeaderSettings
|
return this.$strings.HeaderSettings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showMore() {
|
toggleShowMore() {
|
||||||
this.sideDrawerOpen = true
|
this.sideDrawerOpen = !this.sideDrawerOpen
|
||||||
},
|
},
|
||||||
setDeveloperMode() {
|
setDeveloperMode() {
|
||||||
var value = !this.$store.state.developerMode
|
var value = !this.$store.state.developerMode
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
|
|
||||||
|
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,30 +7,30 @@
|
|||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end py-2">
|
<div class="flex items-end py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
<ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||||
<p class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
@@ -49,33 +49,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
|
<ui-toggle-switch labeledBy="settings-home-page-uses-bookshelf" v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
<span id="settings-home-page-uses-bookshelf">{{ $strings.LabelSettingsHomePageBookshelfView }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
|
<ui-toggle-switch labeledBy="settings-library-uses-bookshelf" v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
<span id="settings-library-uses-bookshelf">{{ $strings.LabelSettingsLibraryBookshelfView }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelSettingsDateFormat }}</p>
|
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguageDefaultServer }}</p>
|
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
||||||
<ui-dropdown ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,20 +83,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsParseSubtitles }}
|
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsFindCovers }}
|
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -109,50 +107,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
<ui-toggle-switch labeledBy="settings-overdrive-media-markers" v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
<span id="settings-overdrive-media-markers">{{ $strings.LabelSettingsOverdriveMediaMarkers }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-audio-metadata" v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
<span id="settings-prefer-audio-metadata">{{ $strings.LabelSettingsPreferAudioMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-opf-metadata" v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
<span id="settings-prefer-opf-metadata">{{ $strings.LabelSettingsPreferOPFMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsDisableWatcher }}
|
<span id="settings-disable-watcher">{{ $strings.LabelSettingsDisableWatcher }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -163,11 +161,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="showExperimentalFeatures" />
|
<ui-toggle-switch labeledBy="settings-experimental-features" v-model="showExperimentalFeatures" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsExperimentalFeatures }}
|
<span id="settings-experimental-features">{{ $strings.LabelSettingsExperimentalFeatures }}</span>
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
<a :aria-label="$strings.LabelSettingsExperimentalFeaturesHelp" href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -175,10 +173,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
<ui-toggle-switch labeledBy="settings-enable-e-reader" v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsEnableEReader }}
|
<span id="settings-enable-e-reader">{{ $strings.LabelSettingsEnableEReader }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -201,9 +199,9 @@
|
|||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<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 rounded-md">
|
||||||
|
<div class="sticky top-0 left-0 w-full h-full flex items-center justify-center" style="max-height: 80vh">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</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,171 @@
|
|||||||
|
<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 rounded-md">
|
||||||
|
<div class="sticky top-0 left-0 w-full h-full flex items-center justify-center" style="max-height: 80vh">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@@ -61,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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+105
-22
@@ -34,6 +34,9 @@
|
|||||||
|
|
||||||
<template v-if="!isVideo">
|
<template v-if="!isVideo">
|
||||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||||
|
<p v-else-if="musicArtists.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
|
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
||||||
|
</p>
|
||||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
@@ -59,6 +62,38 @@
|
|||||||
{{ publishedYear }}
|
{{ publishedYear }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="musicAlbum" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicAlbum }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicAlbumArtist }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicTrackPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicDiscPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex py-0.5" v-if="genres.length">
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
@@ -70,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length" class="flex py-0.5">
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,6 +167,7 @@
|
|||||||
<span v-show="!isStreaming" class="material-icons text-2xl -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 text-2xl -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 }}
|
||||||
@@ -150,11 +186,11 @@
|
|||||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
<ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast && userCanUpdate" :text="$strings.LabelCollections" direction="top">
|
<ui-tooltip v-if="showCollectionsButton" :text="$strings.LabelCollections" direction="top">
|
||||||
<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>
|
||||||
|
|
||||||
@@ -173,7 +209,7 @@
|
|||||||
|
|
||||||
<!-- RSS feed -->
|
<!-- RSS feed -->
|
||||||
<ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
|
<ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +235,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||||
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -222,7 +257,7 @@ export default {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
libraryItem: item,
|
libraryItem: item,
|
||||||
rssFeedUrl: item.rssFeedUrl || null
|
rssFeed: item.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -234,7 +269,6 @@ export default {
|
|||||||
podcastFeedEpisodes: [],
|
podcastFeedEpisodes: [],
|
||||||
episodesDownloading: [],
|
episodesDownloading: [],
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showRssFeedModal: false,
|
|
||||||
showBookmarksModal: false
|
showBookmarksModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -263,12 +297,18 @@ export default {
|
|||||||
isDeveloperMode() {
|
isDeveloperMode() {
|
||||||
return this.$store.state.developerMode
|
return this.$store.state.developerMode
|
||||||
},
|
},
|
||||||
|
isBook() {
|
||||||
|
return this.libraryItem.mediaType === 'book'
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryItem.mediaType === 'podcast'
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isVideo() {
|
isVideo() {
|
||||||
return this.libraryItem.mediaType === 'video'
|
return this.libraryItem.mediaType === 'video'
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryItem.mediaType === 'music'
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
@@ -276,11 +316,12 @@ export default {
|
|||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
invalidAudioFiles() {
|
invalidAudioFiles() {
|
||||||
if (this.isPodcast || this.isVideo) return []
|
if (!this.isBook) return []
|
||||||
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
if (this.isMissing || this.isInvalid) return false
|
if (this.isMissing || this.isInvalid) return false
|
||||||
|
if (this.isMusic) return !!this.audioFile
|
||||||
if (this.isVideo) return !!this.videoFile
|
if (this.isVideo) return !!this.videoFile
|
||||||
if (this.isPodcast) return this.podcastEpisodes.length
|
if (this.isPodcast) return this.podcastEpisodes.length
|
||||||
return this.tracks.length
|
return this.tracks.length
|
||||||
@@ -338,6 +379,25 @@ export default {
|
|||||||
authors() {
|
authors() {
|
||||||
return this.mediaMetadata.authors || []
|
return this.mediaMetadata.authors || []
|
||||||
},
|
},
|
||||||
|
musicArtists() {
|
||||||
|
return this.mediaMetadata.artists || []
|
||||||
|
},
|
||||||
|
musicAlbum() {
|
||||||
|
return this.mediaMetadata.album || ''
|
||||||
|
},
|
||||||
|
musicAlbumArtist() {
|
||||||
|
return this.mediaMetadata.albumArtist || ''
|
||||||
|
},
|
||||||
|
musicTrackPretty() {
|
||||||
|
if (!this.mediaMetadata.trackNumber) return null
|
||||||
|
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||||
|
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||||
|
},
|
||||||
|
musicDiscPretty() {
|
||||||
|
if (!this.mediaMetadata.discNumber) return null
|
||||||
|
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||||
|
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||||
|
},
|
||||||
narrators() {
|
narrators() {
|
||||||
return this.mediaMetadata.narrators || []
|
return this.mediaMetadata.narrators || []
|
||||||
},
|
},
|
||||||
@@ -346,7 +406,7 @@ export default {
|
|||||||
},
|
},
|
||||||
seriesList() {
|
seriesList() {
|
||||||
return this.series.map((se) => {
|
return this.series.map((se) => {
|
||||||
var text = se.name
|
let text = se.name
|
||||||
if (se.sequence) text += ` #${se.sequence}`
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
return {
|
return {
|
||||||
...se,
|
...se,
|
||||||
@@ -355,13 +415,22 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
durationPretty() {
|
durationPretty() {
|
||||||
if (!this.tracks.length) return 'N/A'
|
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||||
return this.$elapsedPretty(this.media.duration)
|
|
||||||
|
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||||
|
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||||
|
return this.$elapsedPretty(this.duration)
|
||||||
},
|
},
|
||||||
duration() {
|
duration() {
|
||||||
if (!this.tracks.length) return 0
|
if (!this.tracks.length && !this.audioFile) return 0
|
||||||
return this.media.duration
|
return this.media.duration
|
||||||
},
|
},
|
||||||
|
totalPodcastDuration() {
|
||||||
|
if (!this.podcastEpisodes.length) return 0
|
||||||
|
let totalDuration = 0
|
||||||
|
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
||||||
|
return totalDuration
|
||||||
|
},
|
||||||
sizePretty() {
|
sizePretty() {
|
||||||
return this.$bytesPretty(this.media.size)
|
return this.$bytesPretty(this.media.size)
|
||||||
},
|
},
|
||||||
@@ -374,6 +443,10 @@ export default {
|
|||||||
videoFile() {
|
videoFile() {
|
||||||
return this.media.videoFile
|
return this.media.videoFile
|
||||||
},
|
},
|
||||||
|
audioFile() {
|
||||||
|
// Music track
|
||||||
|
return this.media.audioFile
|
||||||
|
},
|
||||||
showExperimentalReadAlert() {
|
showExperimentalReadAlert() {
|
||||||
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
||||||
},
|
},
|
||||||
@@ -381,6 +454,7 @@ export default {
|
|||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
|
if (this.isMusic) return null
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
@@ -388,7 +462,7 @@ export default {
|
|||||||
},
|
},
|
||||||
userTimeRemaining() {
|
userTimeRemaining() {
|
||||||
if (!this.userMediaProgress) return 0
|
if (!this.userMediaProgress) return 0
|
||||||
var duration = this.userMediaProgress.duration || this.duration
|
const duration = this.userMediaProgress.duration || this.duration
|
||||||
return duration - this.userMediaProgress.currentTime
|
return duration - this.userMediaProgress.currentTime
|
||||||
},
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
@@ -419,14 +493,17 @@ export default {
|
|||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
showRssFeedBtn() {
|
showRssFeedBtn() {
|
||||||
if (!this.rssFeedUrl && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
|
if (!this.rssFeed && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
|
||||||
|
|
||||||
// If rss feed is open then show feed url to users otherwise just show to admins
|
// If rss feed is open then show feed url to users otherwise just show to admins
|
||||||
return this.userIsAdminOrUp || this.rssFeedUrl
|
return this.userIsAdminOrUp || this.rssFeed
|
||||||
},
|
},
|
||||||
showQueueBtn() {
|
showQueueBtn() {
|
||||||
if (this.isPodcast || this.isVideo) return false
|
if (!this.isBook) return false
|
||||||
return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
|
return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
|
||||||
|
},
|
||||||
|
showCollectionsButton() {
|
||||||
|
return this.isBook && this.userCanUpdate
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -531,14 +608,14 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
playItem(startTime = null) {
|
playItem(startTime = null) {
|
||||||
var episodeId = null
|
let episodeId = null
|
||||||
const queueItems = []
|
const queueItems = []
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
|
|
||||||
// Find most recent episode unplayed
|
// Find most recent episode unplayed
|
||||||
var episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
|
let episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
|
||||||
var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
||||||
return !podcastProgress || !podcastProgress.isFinished
|
return !podcastProgress || !podcastProgress.isFinished
|
||||||
})
|
})
|
||||||
if (episodeIndex < 0) episodeIndex = 0
|
if (episodeIndex < 0) episodeIndex = 0
|
||||||
@@ -617,7 +694,13 @@ export default {
|
|||||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
},
|
},
|
||||||
clickRSSFeed() {
|
clickRSSFeed() {
|
||||||
this.showRssFeedModal = true
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.libraryItemId,
|
||||||
|
name: this.title,
|
||||||
|
type: 'item',
|
||||||
|
feed: this.rssFeed,
|
||||||
|
hasEpisodesWithoutPubDate: this.podcastEpisodes.some((ep) => !ep.pubDate)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
episodeDownloadQueued(episodeDownload) {
|
episodeDownloadQueued(episodeDownload) {
|
||||||
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
@@ -639,13 +722,13 @@ export default {
|
|||||||
rssFeedOpen(data) {
|
rssFeedOpen(data) {
|
||||||
if (data.entityId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Opened', data)
|
console.log('RSS Feed Opened', data)
|
||||||
this.rssFeedUrl = data.feedUrl
|
this.rssFeed = data
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rssFeedClosed(data) {
|
rssFeedClosed(data) {
|
||||||
if (data.entityId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Closed', data)
|
console.log('RSS Feed Closed', data)
|
||||||
this.rssFeedUrl = null
|
this.rssFeed = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
queueBtnClick() {
|
queueBtnClick() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -15,17 +15,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set series sort by
|
// Set series sort by
|
||||||
if (params.id === 'series') {
|
if (query.filter || query.sort || query.desc) {
|
||||||
if (query.sort) {
|
const isSeries = params.id === 'series'
|
||||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
const settingsUpdate = {
|
||||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
[isSeries ? 'seriesFilterBy' : 'filterBy']: query.filter || undefined,
|
||||||
|
[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
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect }) {
|
async asyncData({ store, params, redirect }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
const library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return redirect(`/oops?message=Library "${libraryId}" not found`)
|
return redirect(`/oops?message=Library "${libraryId}" not found`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, query, app }) {
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
const libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!libraryData) {
|
if (!libraryData) {
|
||||||
return redirect('/oops?message=Library not found')
|
return redirect('/oops?message=Library not found')
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export default {
|
|||||||
return redirect(`/library/${libraryId}`)
|
return redirect(`/library/${libraryId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var series = await app.$axios.$get(`/api/series/${params.id}?include=progress`).catch((error) => {
|
const series = await app.$axios.$get(`/api/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export default {
|
|||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
||||||
this.$store.commit('setServerSettings', serverSettings)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
this.$store.commit('setSource', Source)
|
this.$store.commit('setSource', Source)
|
||||||
this.$store.commit('feeds/setFeeds', feeds)
|
|
||||||
this.$setServerLanguageCode(serverSettings.language)
|
this.$setServerLanguageCode(serverSettings.language)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
@@ -137,6 +136,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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default class PlayerHandler {
|
|||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.isHlsTranscode = false
|
this.isHlsTranscode = false
|
||||||
this.isVideo = false
|
this.isVideo = false
|
||||||
|
this.isMusic = false
|
||||||
this.currentSessionId = null
|
this.currentSessionId = null
|
||||||
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
|
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
@@ -54,10 +55,13 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
|
this.isVideo = libraryItem.mediaType === 'video'
|
||||||
|
this.isMusic = libraryItem.mediaType === 'music'
|
||||||
|
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
|
||||||
this.isVideo = libraryItem.mediaType === 'video'
|
|
||||||
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer(playWhenReady)
|
if (!this.player) this.switchPlayer(playWhenReady)
|
||||||
@@ -140,12 +144,14 @@ export default class PlayerHandler {
|
|||||||
playerStateChange(state) {
|
playerStateChange(state) {
|
||||||
console.log('[PlayerHandler] Player state change', state)
|
console.log('[PlayerHandler] Player state change', state)
|
||||||
this.playerState = state
|
this.playerState = state
|
||||||
|
|
||||||
if (this.playerState === 'PLAYING') {
|
if (this.playerState === 'PLAYING') {
|
||||||
this.setPlaybackRate(this.initialPlaybackRate)
|
this.setPlaybackRate(this.initialPlaybackRate)
|
||||||
this.startPlayInterval()
|
this.startPlayInterval()
|
||||||
} else {
|
} else {
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
||||||
this.ctx.setDuration(this.getDuration())
|
this.ctx.setDuration(this.getDuration())
|
||||||
@@ -252,14 +258,14 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
startPlayInterval() {
|
startPlayInterval() {
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
var lastTick = Date.now()
|
let lastTick = Date.now()
|
||||||
this.playInterval = setInterval(() => {
|
this.playInterval = setInterval(() => {
|
||||||
// Update UI
|
// Update UI
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
var currentTime = this.player.getCurrentTime()
|
const currentTime = this.player.getCurrentTime()
|
||||||
this.ctx.setCurrentTime(currentTime)
|
this.ctx.setCurrentTime(currentTime)
|
||||||
|
|
||||||
var exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
||||||
lastTick = Date.now()
|
lastTick = Date.now()
|
||||||
this.listeningTimeSinceSync += exactTimeElapsed
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
if (this.listeningTimeSinceSync >= 5) {
|
if (this.listeningTimeSinceSync >= 5) {
|
||||||
@@ -269,9 +275,9 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendCloseSession() {
|
sendCloseSession() {
|
||||||
var syncData = null
|
let syncData = null
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
syncData = {
|
syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
@@ -285,12 +291,14 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendProgressSync(currentTime) {
|
sendProgressSync(currentTime) {
|
||||||
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
if (this.isMusic) return
|
||||||
|
|
||||||
|
const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
||||||
if (diffSinceLastSync < 1) return
|
if (diffSinceLastSync < 1) return
|
||||||
|
|
||||||
this.lastSyncTime = currentTime
|
this.lastSyncTime = currentTime
|
||||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
var syncData = {
|
const syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
currentTime
|
currentTime
|
||||||
|
|||||||
+11
-9
@@ -5,18 +5,18 @@ import { supplant } from './utils'
|
|||||||
const defaultCode = 'en-us'
|
const defaultCode = 'en-us'
|
||||||
|
|
||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
'de': 'Deutsch',
|
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
'en-us': 'English',
|
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||||
// 'es': 'Español',
|
// 'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||||
'fr': 'Français',
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': 'Hrvatski',
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': 'Italiano',
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
'pl': 'Polski',
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
'zh-cn': '简体中文 (Simplified Chinese)'
|
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||||
}
|
}
|
||||||
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
||||||
return {
|
return {
|
||||||
text: languageCodeMap[code],
|
text: languageCodeMap[code].label,
|
||||||
value: code
|
value: code
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -73,6 +73,8 @@ async function loadi18n(code) {
|
|||||||
for (const key in Vue.prototype.$strings) {
|
for (const key in Vue.prototype.$strings) {
|
||||||
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
||||||
}
|
}
|
||||||
|
console.log(`dateFnsLocale = ${languageCodeMap[code].dateFnsLocale}`)
|
||||||
|
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
||||||
|
|
||||||
console.log('i18n strings=', Vue.prototype.$strings)
|
console.log('i18n strings=', Vue.prototype.$strings)
|
||||||
Vue.prototype.$eventBus.$emit('change-lang', code)
|
Vue.prototype.$eventBus.$emit('change-lang', code)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Path from 'path'
|
import Path from 'path'
|
||||||
import vClickOutside from 'v-click-outside'
|
import vClickOutside from 'v-click-outside'
|
||||||
import { formatDistance, format, addDays, isDate } from 'date-fns'
|
import { formatDistance, format, addDays, isDate, setDefaultOptions } from 'date-fns'
|
||||||
|
import * as locale from 'date-fns/locale'
|
||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
Vue.prototype.$eventBus = new Vue()
|
|
||||||
|
|
||||||
|
Vue.prototype.$setDateFnsLocale = (localeString) => {
|
||||||
|
if (!locale[localeString]) return 0
|
||||||
|
return setDefaultOptions({ locale: locale[localeString] })
|
||||||
|
}
|
||||||
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 +34,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
|
let 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 +62,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 +113,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 +163,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)
|
||||||
|
|||||||
@@ -54,18 +54,18 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|||||||
if (isNaN(seconds) || seconds === null) return ''
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
seconds = Math.round(seconds)
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
let minutes = Math.floor(seconds / 60)
|
||||||
seconds -= minutes * 60
|
seconds -= minutes * 60
|
||||||
var hours = Math.floor(minutes / 60)
|
let hours = Math.floor(minutes / 60)
|
||||||
minutes -= hours * 60
|
minutes -= hours * 60
|
||||||
|
|
||||||
var days = 0
|
let days = 0
|
||||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||||
days = Math.floor(hours / 24)
|
days = Math.floor(hours / 24)
|
||||||
hours -= days * 24
|
hours -= days * 24
|
||||||
}
|
}
|
||||||
|
|
||||||
var strs = []
|
const strs = []
|
||||||
if (days) strs.push(`${days}d`)
|
if (days) strs.push(`${days}d`)
|
||||||
if (hours) strs.push(`${hours}h`)
|
if (hours) strs.push(`${hours}h`)
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
|
||||||
feeds: []
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
getFeedForItem: state => id => {
|
|
||||||
return state.feeds.find(feed => feed.id === id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
addFeed(state, feed) {
|
|
||||||
var index = state.feeds.findIndex(f => f.id === feed.id)
|
|
||||||
if (index >= 0) state.feeds.splice(index, 1, feed)
|
|
||||||
else state.feeds.push(feed)
|
|
||||||
},
|
|
||||||
removeFeed(state, feed) {
|
|
||||||
state.feeds = state.feeds.filter(f => f.id !== feed.id)
|
|
||||||
},
|
|
||||||
setFeeds(state, feeds) {
|
|
||||||
state.feeds = feeds || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+35
-1
@@ -1,6 +1,7 @@
|
|||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isMobileLandscape: false,
|
isMobileLandscape: false,
|
||||||
|
isMobilePortrait: false,
|
||||||
showBatchCollectionModal: false,
|
showBatchCollectionModal: false,
|
||||||
showCollectionsModal: false,
|
showCollectionsModal: false,
|
||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
@@ -8,14 +9,17 @@ export const state = () => ({
|
|||||||
showEditPlaylistModal: false,
|
showEditPlaylistModal: false,
|
||||||
showEditPodcastEpisode: false,
|
showEditPodcastEpisode: false,
|
||||||
showViewPodcastEpisodeModal: false,
|
showViewPodcastEpisodeModal: false,
|
||||||
|
showRSSFeedOpenCloseModal: false,
|
||||||
showConfirmPrompt: false,
|
showConfirmPrompt: false,
|
||||||
confirmPromptOptions: null,
|
confirmPromptOptions: null,
|
||||||
showEditAuthorModal: false,
|
showEditAuthorModal: false,
|
||||||
|
rssFeedEntity: null,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
selectedPlaylistItems: null,
|
selectedPlaylistItems: null,
|
||||||
selectedPlaylist: null,
|
selectedPlaylist: null,
|
||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
selectedAuthor: null,
|
selectedAuthor: null,
|
||||||
|
selectedMediaItems: [],
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
isChromecastInitialized: false, // Script loadeds
|
isChromecastInitialized: false, // Script loadeds
|
||||||
showBatchQuickMatchModal: false,
|
showBatchQuickMatchModal: false,
|
||||||
@@ -64,13 +68,17 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
updateWindowSize(state, { width, height }) {
|
updateWindowSize(state, { width, height }) {
|
||||||
state.isMobile = width < 640 || height < 640
|
state.isMobile = width < 640 || height < 640
|
||||||
state.isMobileLandscape = state.isMobile && height > width
|
state.isMobileLandscape = state.isMobile && height < width
|
||||||
|
state.isMobilePortrait = state.isMobile && height >= width
|
||||||
},
|
},
|
||||||
setShowCollectionsModal(state, val) {
|
setShowCollectionsModal(state, val) {
|
||||||
state.showBatchCollectionModal = false
|
state.showBatchCollectionModal = false
|
||||||
@@ -95,6 +103,13 @@ export const mutations = {
|
|||||||
setShowViewPodcastEpisodeModal(state, val) {
|
setShowViewPodcastEpisodeModal(state, val) {
|
||||||
state.showViewPodcastEpisodeModal = val
|
state.showViewPodcastEpisodeModal = val
|
||||||
},
|
},
|
||||||
|
setShowRSSFeedOpenCloseModal(state, val) {
|
||||||
|
state.showRSSFeedOpenCloseModal = val
|
||||||
|
},
|
||||||
|
setRSSFeedOpenCloseModal(state, entity) {
|
||||||
|
state.rssFeedEntity = entity
|
||||||
|
state.showRSSFeedOpenCloseModal = true
|
||||||
|
},
|
||||||
setShowConfirmPrompt(state, val) {
|
setShowConfirmPrompt(state, val) {
|
||||||
state.showConfirmPrompt = val
|
state.showConfirmPrompt = val
|
||||||
},
|
},
|
||||||
@@ -134,5 +149,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,7 +17,6 @@ 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,
|
||||||
@@ -29,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
|
||||||
},
|
},
|
||||||
@@ -217,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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ export const state = () => ({
|
|||||||
folderLastUpdate: 0,
|
folderLastUpdate: 0,
|
||||||
filterData: null,
|
filterData: null,
|
||||||
numUserPlaylists: 0,
|
numUserPlaylists: 0,
|
||||||
seriesSortBy: 'name',
|
|
||||||
seriesSortDesc: false,
|
|
||||||
seriesFilterBy: 'all',
|
|
||||||
collections: [],
|
collections: [],
|
||||||
userPlaylists: []
|
userPlaylists: []
|
||||||
})
|
})
|
||||||
@@ -73,8 +70,8 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
loadFolders({ state, commit }) {
|
loadFolders({ state, commit }) {
|
||||||
if (state.folders.length) {
|
if (state.folders.length) {
|
||||||
var lastCheck = Date.now() - state.folderLastUpdate
|
const lastCheck = Date.now() - state.folderLastUpdate
|
||||||
if (lastCheck < 1000 * 60 * 10) { // 10 minutes
|
if (lastCheck < 1000 * 5) { // 5 seconds
|
||||||
// Folders up to date
|
// Folders up to date
|
||||||
return state.folders
|
return state.folders
|
||||||
}
|
}
|
||||||
@@ -86,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)
|
||||||
@@ -151,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) => {
|
||||||
@@ -312,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
|
||||||
},
|
},
|
||||||
|
|||||||
+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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+113
-84
@@ -20,9 +20,10 @@
|
|||||||
"ButtonCreate": "Ertsellen",
|
"ButtonCreate": "Ertsellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
|
"ButtonEdit": "Bearbeiten",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
"ButtonForceReScan": "Erzwinge einen Neu-Scan",
|
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
|
||||||
"ButtonFullPath": "Vollständiger Pfad",
|
"ButtonFullPath": "Vollständiger Pfad",
|
||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
@@ -30,21 +31,21 @@
|
|||||||
"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": "Online-Abgleich aller Autoren",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online-Abgleich aller Hörbücher",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Vergiss es",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOpenManager": "Manager öffnen",
|
"ButtonOpenManager": "Manager öffnen",
|
||||||
"ButtonPlay": "Abspielen",
|
"ButtonPlay": "Abspielen",
|
||||||
"ButtonPlaying": "Spielt",
|
"ButtonPlaying": "Spielt",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Wiedergabelisten",
|
||||||
"ButtonPurgeAllCache": "Bereinige alle Zwischenspeicher",
|
"ButtonPurgeAllCache": "Lösche alle Zwischenspeicher",
|
||||||
"ButtonPurgeItemsCache": "Bereinige den Hörbuch/Podcast-Zwischenspeicher",
|
"ButtonPurgeItemsCache": "Lösche Medien-Zwischenspeicher",
|
||||||
"ButtonPurgeMediaProgress": "Bereinige die Hörfortschritte",
|
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
||||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
@@ -60,12 +61,13 @@
|
|||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||||
"ButtonScan": "Durchsuchen",
|
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||||
"ButtonScanLibrary": "Bibliothek durchsuchen",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||||
"ButtonSeries": "Serien",
|
"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",
|
||||||
@@ -74,13 +76,15 @@
|
|||||||
"ButtonUploadBackup": "Sicherung hochladen",
|
"ButtonUploadBackup": "Sicherung hochladen",
|
||||||
"ButtonUploadCover": "Titelbild hochladen",
|
"ButtonUploadCover": "Titelbild hochladen",
|
||||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||||
|
"ButtonUserDelete": "Benutzer {0} löschen",
|
||||||
|
"ButtonUserEdit": "Benutzer {0} bearbeiten",
|
||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"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",
|
||||||
@@ -93,7 +97,8 @@
|
|||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
"HeaderItemFiles": "Objekt-Dateien",
|
"HeaderItemFiles": "Medien-Dateien",
|
||||||
|
"HeaderItemMetadataUtils": "Metadaten",
|
||||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Letzte Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
@@ -103,7 +108,10 @@
|
|||||||
"HeaderListeningStats": "Hörstatistiken",
|
"HeaderListeningStats": "Hörstatistiken",
|
||||||
"HeaderLogin": "Anmeldung",
|
"HeaderLogin": "Anmeldung",
|
||||||
"HeaderLogs": "Protokolle",
|
"HeaderLogs": "Protokolle",
|
||||||
"HeaderMatch": "Online-Abgleich",
|
"HeaderManageGenres": "Kategorien verwalten",
|
||||||
|
"HeaderManageTags": "Tags verwalten",
|
||||||
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
|
"HeaderMatch": "Metadaten",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@@ -112,8 +120,8 @@
|
|||||||
"HeaderOtherFiles": "Sonstige Dateien",
|
"HeaderOtherFiles": "Sonstige Dateien",
|
||||||
"HeaderPermissions": "Berechtigungen",
|
"HeaderPermissions": "Berechtigungen",
|
||||||
"HeaderPlayerQueue": "Spieler Warteschlange",
|
"HeaderPlayerQueue": "Spieler Warteschlange",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Wiedergabeliste",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
||||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||||
"HeaderPreviewCover": "Vorschau Titelbild",
|
"HeaderPreviewCover": "Vorschau Titelbild",
|
||||||
"HeaderRemoveEpisode": "Episode löschen",
|
"HeaderRemoveEpisode": "Episode löschen",
|
||||||
@@ -141,7 +149,7 @@
|
|||||||
"HeaderUpdateDetails": "Details aktualisieren",
|
"HeaderUpdateDetails": "Details aktualisieren",
|
||||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||||
"HeaderUsers": "Benutzer",
|
"HeaderUsers": "Benutzer",
|
||||||
"HeaderYourStats": "Eigene Statistik",
|
"HeaderYourStats": "Eigene Statistiken",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
@@ -149,11 +157,12 @@
|
|||||||
"LabelActivity": "Aktivitäten",
|
"LabelActivity": "Aktivitäten",
|
||||||
"LabelAddedAt": "Hinzugefügt am",
|
"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} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
|
||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
||||||
@@ -200,7 +209,7 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExplicit": "Explizit <br />(Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
"LabelFileBirthtime": "Datei Geburtsdatum",
|
||||||
@@ -229,7 +238,7 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
"LabelItem": "Hörbuch/Podcast",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Informationen",
|
"LabelLogLevelInfo": "Informationen",
|
||||||
"LabelLogLevelWarn": "Warnungen",
|
"LabelLogLevelWarn": "Warnungen",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
||||||
"LabelMarkSeries": "Serien markieren als",
|
|
||||||
"LabelMediaPlayer": "Mediaplayer",
|
"LabelMediaPlayer": "Mediaplayer",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMetadataProvider": "Metadatenanbieter",
|
"LabelMetadataProvider": "Metadatenanbieter",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
||||||
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
||||||
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
||||||
|
"LabelOverwrite": "Überschreiben",
|
||||||
"LabelPassword": "Passwort",
|
"LabelPassword": "Passwort",
|
||||||
"LabelPath": "Pfad",
|
"LabelPath": "Pfad",
|
||||||
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
||||||
@@ -287,11 +296,11 @@
|
|||||||
"LabelPermissionsUpdate": "Aktualisieren",
|
"LabelPermissionsUpdate": "Aktualisieren",
|
||||||
"LabelPermissionsUpload": "Hochladen",
|
"LabelPermissionsUpload": "Hochladen",
|
||||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Wiedergabelisten",
|
||||||
"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",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
@@ -314,7 +324,7 @@
|
|||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"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",
|
||||||
@@ -324,29 +334,29 @@
|
|||||||
"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 Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium 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",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
||||||
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Hörbuchordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
||||||
"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 Tags werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. Wenn keine ID3 Tags 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 online neu abgestimmte Metadaten alle schon vorhandenen Metadaten eines Mediums. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
||||||
"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 verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Medium selber. In dieser sind verschiedene Metadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird der Ordnername verwendet.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-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 Online-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 Mediumtitel \"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 Medienordner 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 Medium 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 Medienordner 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 Medium 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",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@@ -362,7 +372,7 @@
|
|||||||
"LabelStatsDaysListened": "Gehörte Tage",
|
"LabelStatsDaysListened": "Gehörte Tage",
|
||||||
"LabelStatsHours": "Stunden",
|
"LabelStatsHours": "Stunden",
|
||||||
"LabelStatsInARow": "nacheinander",
|
"LabelStatsInARow": "nacheinander",
|
||||||
"LabelStatsItemsFinished": "Gehörte Hörbücher/Podcasts",
|
"LabelStatsItemsFinished": "Gehörte Medien",
|
||||||
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
||||||
"LabelStatsMinutes": "Minuten",
|
"LabelStatsMinutes": "Minuten",
|
||||||
"LabelStatsMinutesListening": "Gehörte Minuten",
|
"LabelStatsMinutesListening": "Gehörte Minuten",
|
||||||
@@ -381,14 +391,17 @@
|
|||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||||
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||||
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
||||||
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Hörbuchdastei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
||||||
"LabelTotalDuration": "Gesamtdauer",
|
"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",
|
||||||
@@ -398,8 +411,8 @@
|
|||||||
"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",
|
||||||
@@ -409,29 +422,40 @@
|
|||||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
"LabelYourProgress": "Fortschritt",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageAddToPlayerQueue": "Add to player queue",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"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.",
|
"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 Sicherungen werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder gespeichert <code>/metadata/items</code> & <code>/metadata/authors</code>. Die Sicherungen enthalten keine Dateien welche in Ihren Bibliotheksordnern gespeichert sind.",
|
"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 (Medien-Ordnern) 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": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"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?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||||
"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}\"?",
|
"MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?",
|
||||||
|
"MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{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!",
|
||||||
@@ -441,18 +465,19 @@
|
|||||||
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
"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 Medien",
|
||||||
|
"MessageItemsUpdated": "{0} Medien aktualisiert",
|
||||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse 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!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Hörbuchkapiteln ohne Anpassung der Zeitangaben",
|
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||||
"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",
|
||||||
@@ -465,8 +490,8 @@
|
|||||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||||
"MessageNoGenres": "Keine Kategorien",
|
"MessageNoGenres": "Keine Kategorien",
|
||||||
"MessageNoIssues": "Keine Probleme",
|
"MessageNoIssues": "Keine Probleme",
|
||||||
"MessageNoItems": "Keine Elemente/Einträge",
|
"MessageNoItems": "Keine Medien",
|
||||||
"MessageNoItemsFound": "Keine Elemente/Einträge gefunden",
|
"MessageNoItemsFound": "Keine Medien gefunden",
|
||||||
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
||||||
"MessageNoLogs": "Keine Protokolle",
|
"MessageNoLogs": "Keine Protokolle",
|
||||||
"MessageNoMediaProgress": "Kein Medienfortschritt",
|
"MessageNoMediaProgress": "Kein Medienfortschritt",
|
||||||
@@ -474,25 +499,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}\"",
|
||||||
|
"MessageNoSeries": "Keine Serien",
|
||||||
|
"MessageNoTags": "Keine Tags",
|
||||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||||
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
"MessageOr": "oder",
|
"MessageOr": "oder",
|
||||||
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||||
"MessagePlayChapter": "Kapitelanfang anhören",
|
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
"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 Online-Abgleichs, 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",
|
"MessageRemoveChapter": "Kapitel löschen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"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",
|
||||||
@@ -501,20 +531,20 @@
|
|||||||
"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} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums 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",
|
||||||
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Hörbuchs nicht überschreiten.",
|
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
|
||||||
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
||||||
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Hörbuch behandelt.",
|
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.",
|
||||||
"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",
|
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
|
||||||
"PlaceholderSearch": "Suche...",
|
"PlaceholderSearch": "Suche...",
|
||||||
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
||||||
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
||||||
@@ -539,21 +569,23 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
||||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||||
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Fehler beim Entfernen der Medien aus der Sammlung",
|
||||||
|
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
|
||||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||||
"ToastCollectionRemoveSuccess": "Sammlung gelöscht",
|
"ToastCollectionRemoveSuccess": "Sammlung gelöscht",
|
||||||
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
||||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||||
"ToastItemCoverUpdateFailed": "Aktualisierung des Titelbildes fehlgeschlagen",
|
"ToastItemCoverUpdateFailed": "Fehler bei der Aktualisierung des Titelbildes",
|
||||||
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
|
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
|
||||||
"ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
|
"ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
|
||||||
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
|
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Keine Aktualisierung für Artikeldetails erforderlichs",
|
"ToastItemDetailsUpdateUnneeded": "Keine Aktualisierung für die Artikeldetails erforderlich",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Als \"abgeschlossen zu markieren\" ist fehlgeschlagen",
|
"ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Mediums als \"Beendet\"",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Artikel/Eintrag als fertig markiert",
|
"ToastItemMarkedAsFinishedSuccess": "Medium als \"Beendet\" markiert",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung als \"Nicht Fertig\"",
|
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Mediums als \"Nicht Beendet\"",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Artikel/Eintrag als \"Nicht Fertig\" markiert",
|
"ToastItemMarkedAsNotFinishedSuccess": "Medium als \"Nicht Beendet\" markiert",
|
||||||
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
|
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
|
||||||
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
|
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
|
||||||
"ToastLibraryDeleteFailed": "Bibliothek konnte nicht gelöscht werden",
|
"ToastLibraryDeleteFailed": "Bibliothek konnte nicht gelöscht werden",
|
||||||
@@ -562,28 +594,25 @@
|
|||||||
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
||||||
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistCreateSuccess": "Wiedergabeliste erstellt",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistRemoveFailed": "Löschen der Wiedergabeliste fehlgeschlagen",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistRemoveSuccess": "Wiedergabeliste gelöscht",
|
||||||
|
"ToastPlaylistUpdateFailed": "Aktualisieren der Wiedergabeliste fehlgeschlagen",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
|
||||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||||
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
"ToastRemoveItemFromCollectionFailed": "Löschen des Mediums aus der Sammlung fehlgeschlagen",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Element/Eintrag aus der Sammlung entfernt",
|
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||||
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||||
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
"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",
|
|
||||||
"WeekdayMonday": "Montag",
|
|
||||||
"WeekdaySaturday": "Samstag",
|
|
||||||
"WeekdaySunday": "Sonntag",
|
|
||||||
"WeekdayThursday": "Donnerstag",
|
|
||||||
"WeekdayTuesday": "Dienstag",
|
|
||||||
"WeekdayWednesday": "Mittwoch"
|
|
||||||
}
|
}
|
||||||
+39
-10
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Create",
|
"ButtonCreate": "Create",
|
||||||
"ButtonCreateBackup": "Create Backup",
|
"ButtonCreateBackup": "Create Backup",
|
||||||
"ButtonDelete": "Delete",
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
"ButtonForceReScan": "Force Re-Scan",
|
"ButtonForceReScan": "Force Re-Scan",
|
||||||
@@ -65,6 +66,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",
|
||||||
@@ -74,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML File",
|
"ButtonUploadOPMLFile": "Upload OPML File",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "View All",
|
"ButtonViewAll": "View All",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -94,6 +98,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",
|
||||||
@@ -103,6 +108,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",
|
||||||
@@ -154,6 +162,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items 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)",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMarkSeries": "Mark Series",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"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",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -341,7 +351,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",
|
||||||
@@ -389,6 +399,9 @@
|
|||||||
"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",
|
||||||
@@ -422,16 +435,27 @@
|
|||||||
"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}?",
|
||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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}\"?",
|
"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!",
|
||||||
@@ -442,6 +466,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...",
|
||||||
@@ -474,6 +499,8 @@
|
|||||||
"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",
|
||||||
@@ -481,6 +508,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"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?",
|
||||||
@@ -489,10 +517,12 @@
|
|||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"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",
|
||||||
@@ -539,6 +569,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",
|
||||||
@@ -562,6 +594,8 @@
|
|||||||
"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",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
@@ -572,18 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
"ToastSessionDeleteSuccess": "Session deleted",
|
"ToastSessionDeleteSuccess": "Session deleted",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
+39
-10
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Create",
|
"ButtonCreate": "Create",
|
||||||
"ButtonCreateBackup": "Create Backup",
|
"ButtonCreateBackup": "Create Backup",
|
||||||
"ButtonDelete": "Delete",
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
"ButtonForceReScan": "Force Re-Scan",
|
"ButtonForceReScan": "Force Re-Scan",
|
||||||
@@ -65,6 +66,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",
|
||||||
@@ -74,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML File",
|
"ButtonUploadOPMLFile": "Upload OPML File",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "View All",
|
"ButtonViewAll": "View All",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -94,6 +98,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",
|
||||||
@@ -103,6 +108,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",
|
||||||
@@ -154,6 +162,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items 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)",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMarkSeries": "Mark Series",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"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",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -341,7 +351,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",
|
||||||
@@ -389,6 +399,9 @@
|
|||||||
"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",
|
||||||
@@ -422,16 +435,27 @@
|
|||||||
"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}?",
|
||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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}\"?",
|
"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!",
|
||||||
@@ -442,6 +466,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...",
|
||||||
@@ -474,6 +499,8 @@
|
|||||||
"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",
|
||||||
@@ -481,6 +508,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"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?",
|
||||||
@@ -489,10 +517,12 @@
|
|||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"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",
|
||||||
@@ -539,6 +569,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",
|
||||||
@@ -562,6 +594,8 @@
|
|||||||
"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",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
@@ -572,18 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
"ToastSessionDeleteSuccess": "Session deleted",
|
"ToastSessionDeleteSuccess": "Session deleted",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
+98
-69
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Créer",
|
"ButtonCreate": "Créer",
|
||||||
"ButtonCreateBackup": "Créer une Sauvegarde",
|
"ButtonCreateBackup": "Créer une Sauvegarde",
|
||||||
"ButtonDelete": "Effacer",
|
"ButtonDelete": "Effacer",
|
||||||
|
"ButtonEdit": "Editer",
|
||||||
"ButtonEditChapters": "Editer Chapitre",
|
"ButtonEditChapters": "Editer Chapitre",
|
||||||
"ButtonEditPodcast": "Editer Podcast",
|
"ButtonEditPodcast": "Editer Podcast",
|
||||||
"ButtonForceReScan": "Forcer un Re-Scan",
|
"ButtonForceReScan": "Forcer un Re-Scan",
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
"ButtonOpenManager": "Ouvrir le Gestionnaire",
|
"ButtonOpenManager": "Ouvrir le Gestionnaire",
|
||||||
"ButtonPlay": "Ecouter",
|
"ButtonPlay": "Ecouter",
|
||||||
"ButtonPlaying": "En Lecture",
|
"ButtonPlaying": "En Lecture",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Listes de Lecture",
|
||||||
"ButtonPurgeAllCache": "Purger Tout le Cache",
|
"ButtonPurgeAllCache": "Purger Tout le Cache",
|
||||||
"ButtonPurgeItemsCache": "Purger le Cache des Articles",
|
"ButtonPurgeItemsCache": "Purger le Cache des Articles",
|
||||||
"ButtonPurgeMediaProgress": "Purger la Progression des Médias",
|
"ButtonPurgeMediaProgress": "Purger la Progression des Médias",
|
||||||
@@ -50,31 +51,34 @@
|
|||||||
"ButtonQuickMatch": "Recherche Rapide",
|
"ButtonQuickMatch": "Recherche Rapide",
|
||||||
"ButtonRead": "Lire",
|
"ButtonRead": "Lire",
|
||||||
"ButtonRemove": "Supprimer",
|
"ButtonRemove": "Supprimer",
|
||||||
"ButtonRemoveAll": "Supprimer Tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer Tout les Articles de la Bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les Articles de la Bibliothèque",
|
||||||
"ButtonRemoveFromContinueListening": "Supprimer de Continuer à Ecouter",
|
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Supprimer la Série de Continuer la Série",
|
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la Série",
|
||||||
"ButtonReScan": "Re-Scan",
|
"ButtonReScan": "Re-Scan",
|
||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
"ButtonRestore": "Rétablir",
|
"ButtonRestore": "Rétablir",
|
||||||
"ButtonSave": "Sauvegarder",
|
"ButtonSave": "Sauvegarder",
|
||||||
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
||||||
"ButtonSaveTracklist": "Sauvegarder la Tracklist",
|
"ButtonSaveTracklist": "Sauvegarder la liste de lecture",
|
||||||
"ButtonScan": "Scanner",
|
"ButtonScan": "Scanner",
|
||||||
"ButtonScanLibrary": "Scanner la Bibliothèque",
|
"ButtonScanLibrary": "Scanner la Bibliothèque",
|
||||||
"ButtonSearch": "Rechercher",
|
"ButtonSearch": "Rechercher",
|
||||||
"ButtonSelectFolderPath": "Sélectionner le Chemin du Dossier",
|
"ButtonSelectFolderPath": "Sélectionner le Chemin du Dossier",
|
||||||
"ButtonSeries": "Séries",
|
"ButtonSeries": "Séries",
|
||||||
|
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
||||||
"ButtonShiftTimes": "Décaler le Temps",
|
"ButtonShiftTimes": "Décaler le Temps",
|
||||||
"ButtonShow": "Montrer",
|
"ButtonShow": "Afficher",
|
||||||
"ButtonStartM4BEncode": "Démarrer l'Encodage M4B",
|
"ButtonStartM4BEncode": "Démarrer l'encodage M4B",
|
||||||
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées Intégrées",
|
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées Intégrées",
|
||||||
"ButtonSubmit": "Soumettre",
|
"ButtonSubmit": "Soumettre",
|
||||||
"ButtonUpload": "Téléverser",
|
"ButtonUpload": "Téléverser",
|
||||||
"ButtonUploadBackup": "Téléverser une Sauvegarde",
|
"ButtonUploadBackup": "Téléverser une Sauvegarde",
|
||||||
"ButtonUploadCover": "Téléverser une Couverture",
|
"ButtonUploadCover": "Téléverser une Couverture",
|
||||||
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
||||||
"ButtonViewAll": "Voir Tout",
|
"ButtonUserDelete": "Effacer l'utilisateur {0}",
|
||||||
|
"ButtonUserEdit": "Modifier l'utilisateur {0}",
|
||||||
|
"ButtonViewAll": "Afficher Tout",
|
||||||
"ButtonYes": "Oui",
|
"ButtonYes": "Oui",
|
||||||
"HeaderAccount": "Compte",
|
"HeaderAccount": "Compte",
|
||||||
"HeaderAdvanced": "Avancé",
|
"HeaderAdvanced": "Avancé",
|
||||||
@@ -82,7 +86,7 @@
|
|||||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||||
"HeaderAudioTracks": "Pistes Audio",
|
"HeaderAudioTracks": "Pistes Audio",
|
||||||
"HeaderBackups": "Sauvegardes",
|
"HeaderBackups": "Sauvegardes",
|
||||||
"HeaderChangePassword": "Chager le Mot de Passe",
|
"HeaderChangePassword": "Chager le mot de passe",
|
||||||
"HeaderChapters": "Chapitres",
|
"HeaderChapters": "Chapitres",
|
||||||
"HeaderChooseAFolder": "Choisir un Dossier",
|
"HeaderChooseAFolder": "Choisir un Dossier",
|
||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
@@ -94,6 +98,7 @@
|
|||||||
"HeaderFindChapters": "Trouver les Chapitres",
|
"HeaderFindChapters": "Trouver les Chapitres",
|
||||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||||
"HeaderItemFiles": "Fichiers des Articles",
|
"HeaderItemFiles": "Fichiers des Articles",
|
||||||
|
"HeaderItemMetadataUtils": "Outils de Gestion des Métadonnées",
|
||||||
"HeaderLastListeningSession": "Dernière Session d'Ecoute",
|
"HeaderLastListeningSession": "Dernière Session d'Ecoute",
|
||||||
"HeaderLatestEpisodes": "Dernier Episodes",
|
"HeaderLatestEpisodes": "Dernier Episodes",
|
||||||
"HeaderLibraries": "Bibliothèque",
|
"HeaderLibraries": "Bibliothèque",
|
||||||
@@ -103,6 +108,9 @@
|
|||||||
"HeaderListeningStats": "Statistiques d'Ecoute",
|
"HeaderListeningStats": "Statistiques d'Ecoute",
|
||||||
"HeaderLogin": "Connexion",
|
"HeaderLogin": "Connexion",
|
||||||
"HeaderLogs": "Fichiers Journaux",
|
"HeaderLogs": "Fichiers Journaux",
|
||||||
|
"HeaderManageGenres": "Gérer les Genres",
|
||||||
|
"HeaderManageTags": "Gérer les Etiquettes",
|
||||||
|
"HeaderMapDetails": "Edition en Masse",
|
||||||
"HeaderMatch": "Rechercher",
|
"HeaderMatch": "Rechercher",
|
||||||
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
||||||
"HeaderNewAccount": "Nouveau Compte",
|
"HeaderNewAccount": "Nouveau Compte",
|
||||||
@@ -112,8 +120,8 @@
|
|||||||
"HeaderOtherFiles": "Autres Fichiers",
|
"HeaderOtherFiles": "Autres Fichiers",
|
||||||
"HeaderPermissions": "Permissions",
|
"HeaderPermissions": "Permissions",
|
||||||
"HeaderPlayerQueue": "Liste d'Ecoute",
|
"HeaderPlayerQueue": "Liste d'Ecoute",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Liste de Lecture",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Elements de la Liste de Lecture",
|
||||||
"HeaderPodcastsToAdd": "Podcasts à Ajouter",
|
"HeaderPodcastsToAdd": "Podcasts à Ajouter",
|
||||||
"HeaderPreviewCover": "Prévisualiser la Couverture",
|
"HeaderPreviewCover": "Prévisualiser la Couverture",
|
||||||
"HeaderRemoveEpisode": "Supprimer l'Episode",
|
"HeaderRemoveEpisode": "Supprimer l'Episode",
|
||||||
@@ -150,10 +158,11 @@
|
|||||||
"LabelAddedAt": "Date d'Ajout",
|
"LabelAddedAt": "Date d'Ajout",
|
||||||
"LabelAddToCollection": "Ajouter à la Collection",
|
"LabelAddToCollection": "Ajouter à la Collection",
|
||||||
"LabelAddToCollectionBatch": "Ajout de {0} Livres à la Collection",
|
"LabelAddToCollectionBatch": "Ajout de {0} Livres à la Collection",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "Ajouter à la Liste de Lecture",
|
||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "{0} Elements Ajoutés à la Liste de Lecture",
|
||||||
"LabelAll": "Tout",
|
"LabelAll": "Tout",
|
||||||
"LabelAllUsers": "Tous les Utilisateurs",
|
"LabelAllUsers": "Tous les Utilisateurs",
|
||||||
|
"LabelAppend": "Ajouter",
|
||||||
"LabelAuthor": "Auteur",
|
"LabelAuthor": "Auteur",
|
||||||
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
||||||
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Rechercher de Nouveaux Episode après cette Date",
|
"LabelLookForNewEpisodesAfterDate": "Rechercher de Nouveaux Episode après cette Date",
|
||||||
"LabelMarkSeries": "Marquer la Série",
|
|
||||||
"LabelMediaPlayer": "Lecteur Multimédia",
|
"LabelMediaPlayer": "Lecteur Multimédia",
|
||||||
"LabelMediaType": "Type de Média",
|
"LabelMediaType": "Type de Média",
|
||||||
"LabelMetadataProvider": "Fournisseur de Métadonnées",
|
"LabelMetadataProvider": "Fournisseur de Métadonnées",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Nombre de Livres",
|
"LabelNumberOfBooks": "Nombre de Livres",
|
||||||
"LabelNumberOfEpisodes": "Nombre d'Episodes",
|
"LabelNumberOfEpisodes": "Nombre d'Episodes",
|
||||||
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
||||||
|
"LabelOverwrite": "Ecraser",
|
||||||
"LabelPassword": "Mot de Passe",
|
"LabelPassword": "Mot de Passe",
|
||||||
"LabelPath": "Chemin",
|
"LabelPath": "Chemin",
|
||||||
"LabelPermissionsAccessAllLibraries": "Peut Acceder à Toutes les Bibliothèque",
|
"LabelPermissionsAccessAllLibraries": "Peut Acceder à Toutes les Bibliothèque",
|
||||||
@@ -287,7 +296,7 @@
|
|||||||
"LabelPermissionsUpdate": "Peut Mettre à Jour",
|
"LabelPermissionsUpdate": "Peut Mettre à Jour",
|
||||||
"LabelPermissionsUpload": "Peut Téléverser",
|
"LabelPermissionsUpload": "Peut Téléverser",
|
||||||
"LabelPhotoPathURL": "Chemin/URL des photos",
|
"LabelPhotoPathURL": "Chemin/URL des photos",
|
||||||
"LabelPlaylists": "Playlists",
|
"LabelPlaylists": "Listes de Lecture",
|
||||||
"LabelPlayMethod": "Méthode d'Ecoute",
|
"LabelPlayMethod": "Méthode d'Ecoute",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Année d'Edition",
|
"LabelPublishYear": "Année d'Edition",
|
||||||
"LabelRecentlyAdded": "Derniers Ajouts",
|
"LabelRecentlyAdded": "Derniers Ajouts",
|
||||||
"LabelRecentSeries": "Séries Récentes",
|
"LabelRecentSeries": "Séries Récentes",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de Parution",
|
"LabelReleaseDate": "Date de Parution",
|
||||||
"LabelRemoveCover": "Supprimer la Couverture",
|
"LabelRemoveCover": "Supprimer la Couverture",
|
||||||
@@ -389,6 +399,9 @@
|
|||||||
"LabelTotalTimeListened": "Temps d'Ecoute Total",
|
"LabelTotalTimeListened": "Temps d'Ecoute Total",
|
||||||
"LabelTrackFromFilename": "Piste depuis le Fichier",
|
"LabelTrackFromFilename": "Piste depuis le Fichier",
|
||||||
"LabelTrackFromMetadata": "Piste depuis les Métadonnées",
|
"LabelTrackFromMetadata": "Piste depuis les Métadonnées",
|
||||||
|
"LabelTracks": "Pistes",
|
||||||
|
"LabelTracksMultiTrack": "Piste Multiple",
|
||||||
|
"LabelTracksSingleTrack": "Piste Simple",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnknown": "Inconnu",
|
"LabelUnknown": "Inconnu",
|
||||||
"LabelUpdateCover": "Mettre à jour la Couverture",
|
"LabelUpdateCover": "Mettre à jour la Couverture",
|
||||||
@@ -404,16 +417,16 @@
|
|||||||
"LabelUsername": "Nom d'Utilisateur",
|
"LabelUsername": "Nom d'Utilisateur",
|
||||||
"LabelValue": "Valeur",
|
"LabelValue": "Valeur",
|
||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "Voir les Signets",
|
"LabelViewBookmarks": "Afficher les Signets",
|
||||||
"LabelViewChapters": "Voir les Chapitres",
|
"LabelViewChapters": "Afficher les Chapitres",
|
||||||
"LabelViewQueue": "Voir la Liste de Lecture",
|
"LabelViewQueue": "Afficher la Liste de Lecture",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
||||||
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
||||||
"LabelYourBookmarks": "Vos Signets",
|
"LabelYourBookmarks": "Vos Signets",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Vos Listes de Lecture",
|
||||||
"LabelYourProgress": "Votre Progression",
|
"LabelYourProgress": "Votre Progression",
|
||||||
"MessageAddToPlayerQueue": "Add to player queue",
|
"MessageAddToPlayerQueue": "Ajouter en Queue d'Ecoute",
|
||||||
"MessageAppriseDescription": "Nécessite une instance d'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />L'URL de l'API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Nécessite une instance d'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />L'URL de l'API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Les Sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les Sauvegardes n'incluent pas les fichiers de votre bibliothèque.",
|
"MessageBackupsDescription": "Les Sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les Sauvegardes n'incluent pas les fichiers de votre bibliothèque.",
|
||||||
"MessageBatchQuickMatchDescription": "La Recherche par Correspondance Rapide tentera d'ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l'option suivante pour autoriser la Recherche par Correspondance à écraser les données existantes.",
|
"MessageBatchQuickMatchDescription": "La Recherche par Correspondance Rapide tentera d'ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l'option suivante pour autoriser la Recherche par Correspondance à écraser les données existantes.",
|
||||||
@@ -422,16 +435,27 @@
|
|||||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n'est ouvert",
|
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n'est ouvert",
|
||||||
"MessageBookshelfNoSeries": "Vous n'avez aucune séries",
|
"MessageBookshelfNoSeries": "Vous n'avez aucune séries",
|
||||||
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
||||||
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
||||||
"MessageCheckingCron": "Vérification du cron...",
|
"MessageCheckingCron": "Vérification du cron...",
|
||||||
"MessageConfirmDeleteBackup": "Etes vous certain de vouloir supprimer la Sauvegarde de {0}?",
|
"MessageConfirmDeleteBackup": "Etes vous certain de vouloir supprimer la Sauvegarde de {0}?",
|
||||||
"MessageConfirmDeleteLibrary": "Etes vous certain de vouloir supprimer définitivement la bibliothèque \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Etes vous certain de vouloir supprimer définitivement la bibliothèque \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Etes vous certain de vouloir supprimer cette session?",
|
"MessageConfirmDeleteSession": "Etes vous certain de vouloir supprimer cette session?",
|
||||||
"MessageConfirmForceReScan": "Etes vous certain de vouloir lancer une Analyse Forcée?",
|
"MessageConfirmForceReScan": "Etes vous certain de vouloir lancer une Analyse Forcée?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Etes vous certain de vouloir marquer comme terminé tous les livres de cette série?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Etes vous certain de vouloir marquer comme non terminé tous les livres de cette série?",
|
||||||
"MessageConfirmRemoveCollection": "Etes vous certain de vouloir supprimer la collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Etes vous certain de vouloir supprimer la collection \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Etes vous certain de vouloir supprimer la liste de lecture \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Etes vous certain de vouloir renommer le genre \"{0}\" vers \"{1}\" pour tous les articles?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Attention! Un genre similaire avec une casse différente existe déjà \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Etes vous certain de vouloir renommer l'étiquette \"{0}\" vers \"{1}\" pour tous les articles?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Attention! Une étiquette similaire avec une casse différente existe déjà \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Téléchargement de l'épisode",
|
"MessageDownloadingEpisode": "Téléchargement de l'épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l'ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l'ordre correct",
|
||||||
"MessageEmbedFinished": "Intégration Terminée!",
|
"MessageEmbedFinished": "Intégration Terminée!",
|
||||||
@@ -442,6 +466,7 @@
|
|||||||
"MessageImportantNotice": "Information Importante!",
|
"MessageImportantNotice": "Information Importante!",
|
||||||
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
||||||
"MessageItemsSelected": "{0} Articles Sélectionnés",
|
"MessageItemsSelected": "{0} Articles Sélectionnés",
|
||||||
|
"MessageItemsUpdated": "{0} Articles Mis à Jour",
|
||||||
"MessageJoinUsOn": "Rejoignez-nous sur",
|
"MessageJoinUsOn": "Rejoignez-nous sur",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d'écoute l'an dernier",
|
"MessageListeningSessionsInTheLastYear": "{0} sessions d'écoute l'an dernier",
|
||||||
"MessageLoading": "Chargement...",
|
"MessageLoading": "Chargement...",
|
||||||
@@ -474,28 +499,33 @@
|
|||||||
"MessageNoPodcastsFound": "Pas de podcasts trouvés",
|
"MessageNoPodcastsFound": "Pas de podcasts trouvés",
|
||||||
"MessageNoResults": "Pas de Résultats",
|
"MessageNoResults": "Pas de Résultats",
|
||||||
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
||||||
|
"MessageNoSeries": "Pas de Séries",
|
||||||
|
"MessageNoTags": "Pas d'Etiquettes",
|
||||||
"MessageNotYetImplemented": "Non implémenté",
|
"MessageNotYetImplemented": "Non implémenté",
|
||||||
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
||||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "Vous n'avez aucune liste de lecture",
|
||||||
"MessageOr": "ou",
|
"MessageOr": "ou",
|
||||||
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
||||||
"MessagePlayChapter": "Ecouter depuis le début du chapitre",
|
"MessagePlayChapter": "Ecouter depuis le début du chapitre",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n'a pas d'URL de flux RSS à utiliser pour la correspondance",
|
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n'a pas d'URL de flux RSS à utiliser pour la correspondance",
|
||||||
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de '{0}'. N'écrase pas les données présentes à moins que le paramètre 'Préférer les Métadonnées par correspondance' soit activé.",
|
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de '{0}'. N'écrase pas les données présentes à moins que le paramètre 'Préférer les Métadonnées par correspondance' soit activé.",
|
||||||
"MessageRemoveAllItemsWarning": "ATTENTION! Cette action supprimera toute la base de données de la bibliothèque ainsi que les mises à jour ou correspondances qui auraient été effectuées. Cela n'a aucune incidence sur les fichiers de la bibliothèque. Voulez-vous continuer?",
|
"MessageRemoveAllItemsWarning": "ATTENTION! Cette action supprimera toute la base de données de la bibliothèque ainsi que les mises à jour ou correspondances qui auraient été effectuées. Cela n'a aucune incidence sur les fichiers de la bibliothèque. Voulez-vous continuer?",
|
||||||
"MessageRemoveChapter": "Supprimer le chapitre",
|
"MessageRemoveChapter": "Supprimer le chapitre",
|
||||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d'écoute",
|
||||||
"MessageRemoveUserWarning": "Etes-vous certain de vouloir supprimer définitivement l'utilisateur \"{0}\"?",
|
"MessageRemoveUserWarning": "Etes-vous certain de vouloir supprimer définitivement l'utilisateur \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
|
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
|
||||||
|
"MessageResetChaptersConfirm": "Etes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués?",
|
||||||
"MessageRestoreBackupConfirm": "Etes-vous certain de vouloir restaurer la sauvegarde créée le",
|
"MessageRestoreBackupConfirm": "Etes-vous certain de vouloir restaurer la sauvegarde créée le",
|
||||||
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items & /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
|
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items & /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
|
||||||
"MessageSearchResultsFor": "Résultats de recherche pour",
|
"MessageSearchResultsFor": "Résultats de recherche pour",
|
||||||
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
||||||
"MessageStartPlaybackAtTime": "Démarrer la lecture pour \"{0}\" à {1}?",
|
"MessageStartPlaybackAtTime": "Démarrer la lecture pour \"{0}\" à {1}?",
|
||||||
"MessageThinking": "On Réfléchit...",
|
"MessageThinking": "On réfléchit...",
|
||||||
"MessageUploaderItemFailed": "Echec du téléversement",
|
"MessageUploaderItemFailed": "Échec du téléversement",
|
||||||
"MessageUploaderItemSuccess": "Téléversement effectué!",
|
"MessageUploaderItemSuccess": "Téléversement effectué!",
|
||||||
"MessageUploading": "Téléversement...",
|
"MessageUploading": "Téléversement...",
|
||||||
"MessageValidCronExpression": "Expression cron valide",
|
"MessageValidCronExpression": "Expression cron valide",
|
||||||
@@ -514,76 +544,75 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "Les fichiers non-supportés seront ignorés. En sélectionnant ou déponsant un dossier, les autres fichiers qui ne sont pas un dossier contenant un article seront ignorés.",
|
"NoteUploaderUnsupportedFiles": "Les fichiers non-supportés seront ignorés. En sélectionnant ou déponsant un dossier, les autres fichiers qui ne sont pas un dossier contenant un article seront ignorés.",
|
||||||
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||||
"PlaceholderSearch": "Recherche...",
|
"PlaceholderSearch": "Recherche...",
|
||||||
"ToastAccountUpdateFailed": "Echec de la mise à jour du compte",
|
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
||||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||||
"ToastAuthorImageRemoveFailed": "Echec de la suppression de l'image",
|
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l'image",
|
||||||
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
||||||
"ToastAuthorUpdateFailed": "Echec de la mise à jour de l'auteur",
|
"ToastAuthorUpdateFailed": "Échec de la mise à jour de l'auteur",
|
||||||
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
||||||
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
||||||
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
||||||
"ToastBackupCreateFailed": "Echec de la création de sauvegarde",
|
"ToastBackupCreateFailed": "Échec de la création de sauvegarde",
|
||||||
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
||||||
"ToastBackupDeleteFailed": "Echec de la suppression de sauvegarde",
|
"ToastBackupDeleteFailed": "Échec de la suppression de sauvegarde",
|
||||||
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
|
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
|
||||||
"ToastBackupRestoreFailed": "Echec de la restauration de sauvegarde",
|
"ToastBackupRestoreFailed": "Échec de la restauration de sauvegarde",
|
||||||
"ToastBackupUploadFailed": "Echec du téléversement de sauvegarde",
|
"ToastBackupUploadFailed": "Échec du téléversement de sauvegarde",
|
||||||
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
|
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
|
||||||
"ToastBatchUpdateFailed": "Echec de la mise à jour par lot",
|
"ToastBatchUpdateFailed": "Échec de la mise à jour par lot",
|
||||||
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
|
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
|
||||||
"ToastBookmarkCreateFailed": "Echec de la création de signet",
|
"ToastBookmarkCreateFailed": "Échec de la création de signet",
|
||||||
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
||||||
"ToastBookmarkRemoveFailed": "Echec de la suppression de signet",
|
"ToastBookmarkRemoveFailed": "Échec de la suppression de signet",
|
||||||
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
||||||
"ToastBookmarkUpdateFailed": "Echec de la mise à jour de signet",
|
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
|
||||||
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
||||||
"ToastCollectionItemsRemoveFailed": "Echec de la suppression de(s) article(s) de la collection",
|
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||||
|
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
||||||
"ToastCollectionRemoveFailed": "Echec de la suppression de la collection",
|
"ToastCollectionRemoveFailed": "Échec de la suppression de la collection",
|
||||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||||
"ToastCollectionUpdateFailed": "Echec de la mise à jour de la collection",
|
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
||||||
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
||||||
"ToastItemCoverUpdateFailed": "Echec de la mise à jour de la couverture de l'article",
|
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l'article",
|
||||||
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
||||||
"ToastItemDetailsUpdateFailed": "Echec de la mise à jour des détails de l'article",
|
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l'article",
|
||||||
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Echec de l'annotation terminée",
|
"ToastItemMarkedAsFinishedFailed": "Échec de l'annotation terminée",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Echec de l'annotation non-terminée",
|
"ToastItemMarkedAsNotFinishedFailed": "Échec de l'annotation non-terminée",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
||||||
"ToastLibraryCreateFailed": "Echec de la création de bibliothèque",
|
"ToastLibraryCreateFailed": "Échec de la création de bibliothèque",
|
||||||
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
||||||
"ToastLibraryDeleteFailed": "Echec de la suppression de la bibliothèque",
|
"ToastLibraryDeleteFailed": "Échec de la suppression de la bibliothèque",
|
||||||
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
||||||
"ToastLibraryScanFailedToStart": "Echec du démarrage de l'analyse",
|
"ToastLibraryScanFailedToStart": "Échec du démarrage de l'analyse",
|
||||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||||
"ToastLibraryUpdateFailed": "Echec de la mise à jour de la bibliothèque",
|
"ToastLibraryUpdateFailed": "Échec de la mise à jour de la bibliothèque",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistRemoveFailed": "Échec de la suppression de la liste de lecture",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
||||||
"ToastPodcastCreateFailed": "Echec de la création du Podcast",
|
"ToastPlaylistUpdateFailed": "Échec de la mise à jour de la liste de lecture",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
||||||
|
"ToastPodcastCreateFailed": "Échec de la création du Podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast créé",
|
"ToastPodcastCreateSuccess": "Podcast créé",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Echec de la suppression de l'article de la collection",
|
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l'article de la collection",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Echec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
"ToastSessionDeleteFailed": "Echec de la suppression de session",
|
"ToastSeriesUpdateFailed": "Echec de la mise à jour de la série",
|
||||||
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
"ToastSessionDeleteSuccess": "Session supprimée",
|
"ToastSessionDeleteSuccess": "Session supprimée",
|
||||||
"ToastSocketConnected": "WebSocket connectée",
|
"ToastSocketConnected": "WebSocket connecté",
|
||||||
"ToastSocketDisconnected": "WebSocket déconnectée",
|
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||||
"ToastSocketFailedToConnect": "Echec de la connexion WebSocket",
|
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||||
"ToastUserDeleteFailed": "Echec de la suppression de l'utilisateur",
|
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
||||||
"ToastUserDeleteSuccess": "Utilisateur supprimé",
|
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||||
"WeekdayFriday": "Vendredi",
|
|
||||||
"WeekdayMonday": "Lundi",
|
|
||||||
"WeekdaySaturday": "Samedi",
|
|
||||||
"WeekdaySunday": "Dimanche",
|
|
||||||
"WeekdayThursday": "Jeudi",
|
|
||||||
"WeekdayTuesday": "Mardi",
|
|
||||||
"WeekdayWednesday": "Mercredi"
|
|
||||||
}
|
}
|
||||||
+38
-9
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Napravi",
|
"ButtonCreate": "Napravi",
|
||||||
"ButtonCreateBackup": "Napravi backup",
|
"ButtonCreateBackup": "Napravi backup",
|
||||||
"ButtonDelete": "Obriši",
|
"ButtonDelete": "Obriši",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Uredi poglavlja",
|
"ButtonEditChapters": "Uredi poglavlja",
|
||||||
"ButtonEditPodcast": "Uredi podcast",
|
"ButtonEditPodcast": "Uredi podcast",
|
||||||
"ButtonForceReScan": "Prisilno ponovno skeniranje",
|
"ButtonForceReScan": "Prisilno ponovno skeniranje",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"ButtonSearch": "Traži",
|
"ButtonSearch": "Traži",
|
||||||
"ButtonSelectFolderPath": "Odaberi putanju do folder",
|
"ButtonSelectFolderPath": "Odaberi putanju do folder",
|
||||||
"ButtonSeries": "Serije",
|
"ButtonSeries": "Serije",
|
||||||
|
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||||
"ButtonShiftTimes": "Pomakni vremena",
|
"ButtonShiftTimes": "Pomakni vremena",
|
||||||
"ButtonShow": "Prikaži",
|
"ButtonShow": "Prikaži",
|
||||||
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
||||||
@@ -74,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload backup",
|
"ButtonUploadBackup": "Upload backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML Datoteku",
|
"ButtonUploadOPMLFile": "Upload OPML Datoteku",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "Prikaži sve",
|
"ButtonViewAll": "Prikaži sve",
|
||||||
"ButtonYes": "Da",
|
"ButtonYes": "Da",
|
||||||
"HeaderAccount": "Korisnički račun",
|
"HeaderAccount": "Korisnički račun",
|
||||||
@@ -94,6 +98,7 @@
|
|||||||
"HeaderFindChapters": "Pronađi poglavlja",
|
"HeaderFindChapters": "Pronađi poglavlja",
|
||||||
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
||||||
"HeaderItemFiles": "Item Files",
|
"HeaderItemFiles": "Item Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Posljednja Listening Session",
|
"HeaderLastListeningSession": "Posljednja Listening Session",
|
||||||
"HeaderLatestEpisodes": "Najnovije epizode",
|
"HeaderLatestEpisodes": "Najnovije epizode",
|
||||||
"HeaderLibraries": "Biblioteke",
|
"HeaderLibraries": "Biblioteke",
|
||||||
@@ -103,6 +108,9 @@
|
|||||||
"HeaderListeningStats": "Listening Stats",
|
"HeaderListeningStats": "Listening Stats",
|
||||||
"HeaderLogin": "Prijavljivanje",
|
"HeaderLogin": "Prijavljivanje",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Match",
|
"HeaderMatch": "Match",
|
||||||
"HeaderMetadataToEmbed": "Metapodatci za ugradnju",
|
"HeaderMetadataToEmbed": "Metapodatci za ugradnju",
|
||||||
"HeaderNewAccount": "Novi korisnički račun",
|
"HeaderNewAccount": "Novi korisnički račun",
|
||||||
@@ -154,6 +162,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "Svi korisnici",
|
"LabelAllUsers": "Svi korisnici",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
||||||
"LabelMarkSeries": "Označi seriju",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"LabelNumberOfBooks": "Number of Books",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfEpisodes": "# of Episodes",
|
||||||
"LabelOpenRSSFeed": "Otvori RSS Feed",
|
"LabelOpenRSSFeed": "Otvori RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Lozinka",
|
"LabelPassword": "Lozinka",
|
||||||
"LabelPath": "Putanja",
|
"LabelPath": "Putanja",
|
||||||
"LabelPermissionsAccessAllLibraries": "Ima pristup svim bibliotekama",
|
"LabelPermissionsAccessAllLibraries": "Ima pristup svim bibliotekama",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regija",
|
"LabelRegion": "Regija",
|
||||||
"LabelReleaseDate": "Datum izlaska",
|
"LabelReleaseDate": "Datum izlaska",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -389,6 +399,9 @@
|
|||||||
"LabelTotalTimeListened": "Sveukupno vrijeme slušanja",
|
"LabelTotalTimeListened": "Sveukupno vrijeme slušanja",
|
||||||
"LabelTrackFromFilename": "Track iz imena datoteke",
|
"LabelTrackFromFilename": "Track iz imena datoteke",
|
||||||
"LabelTrackFromMetadata": "Track iz metapodataka",
|
"LabelTrackFromMetadata": "Track iz metapodataka",
|
||||||
|
"LabelTracks": "Tracks",
|
||||||
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Tip",
|
"LabelType": "Tip",
|
||||||
"LabelUnknown": "Nepoznato",
|
"LabelUnknown": "Nepoznato",
|
||||||
"LabelUpdateCover": "Aktualiziraj Cover",
|
"LabelUpdateCover": "Aktualiziraj Cover",
|
||||||
@@ -422,16 +435,27 @@
|
|||||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||||
"MessageBookshelfNoSeries": "You have no series",
|
"MessageBookshelfNoSeries": "You have no series",
|
||||||
"MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja audioknjige.",
|
"MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja audioknjige.",
|
||||||
|
"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": "Početak poglavlja je nakon kraja audioknjige.",
|
"MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.",
|
||||||
"MessageCheckingCron": "Provjeravam cron...",
|
"MessageCheckingCron": "Provjeravam cron...",
|
||||||
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
|
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
|
||||||
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||||
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"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": "Preuzimam epizodu",
|
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
||||||
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
||||||
"MessageEmbedFinished": "Embed završen!",
|
"MessageEmbedFinished": "Embed završen!",
|
||||||
@@ -442,6 +466,7 @@
|
|||||||
"MessageImportantNotice": "Važna obavijest!",
|
"MessageImportantNotice": "Važna obavijest!",
|
||||||
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
|
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
|
||||||
"MessageItemsSelected": "{0} odabranih stavki",
|
"MessageItemsSelected": "{0} odabranih stavki",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Pridruži nam se na",
|
"MessageJoinUsOn": "Pridruži nam se na",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} slušanja u prošloj godini",
|
"MessageListeningSessionsInTheLastYear": "{0} slušanja u prošloj godini",
|
||||||
"MessageLoading": "Učitavam...",
|
"MessageLoading": "Učitavam...",
|
||||||
@@ -474,6 +499,8 @@
|
|||||||
"MessageNoPodcastsFound": "Nijedan podcast pronađen",
|
"MessageNoPodcastsFound": "Nijedan podcast pronađen",
|
||||||
"MessageNoResults": "Nema rezultata",
|
"MessageNoResults": "Nema rezultata",
|
||||||
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
||||||
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
|
"MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
|
||||||
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
|
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
|
||||||
@@ -481,6 +508,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
|
||||||
"MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
|
"MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
|
||||||
"MessageRemoveAllItemsWarning": "UPOZORENJE! Ova radnja briše sve stavke iz biblioteke uključujući bilokakve aktualizacije ili matcheve. Ovo ne mjenja vaše lokalne datoteke. Jeste li sigurni?",
|
"MessageRemoveAllItemsWarning": "UPOZORENJE! Ova radnja briše sve stavke iz biblioteke uključujući bilokakve aktualizacije ili matcheve. Ovo ne mjenja vaše lokalne datoteke. Jeste li sigurni?",
|
||||||
@@ -489,10 +517,12 @@
|
|||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||||
"MessageRemoveUserWarning": "Jeste li sigurni da želite trajno obrisati korisnika \"{0}\"?",
|
"MessageRemoveUserWarning": "Jeste li sigurni da želite trajno obrisati korisnika \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Prijavte bugove, zatržite featurese i doprinosite na",
|
"MessageReportBugsAndContribute": "Prijavte bugove, zatržite featurese i doprinosite na",
|
||||||
|
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||||
"MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
|
"MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
|
||||||
"MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
|
"MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
|
||||||
"MessageSearchResultsFor": "Traži rezultate za",
|
"MessageSearchResultsFor": "Traži rezultate za",
|
||||||
"MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
|
"MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||||
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
|
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
|
||||||
"MessageThinking": "Razmišljam...",
|
"MessageThinking": "Razmišljam...",
|
||||||
"MessageUploaderItemFailed": "Upload neuspješan",
|
"MessageUploaderItemFailed": "Upload neuspješan",
|
||||||
@@ -539,6 +569,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Knjižnja bilješka uklonjena",
|
"ToastBookmarkRemoveSuccess": "Knjižnja bilješka uklonjena",
|
||||||
"ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna",
|
"ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna",
|
||||||
"ToastBookmarkUpdateSuccess": "Knjižna bilješka aktualizirana",
|
"ToastBookmarkUpdateSuccess": "Knjižna bilješka aktualizirana",
|
||||||
|
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||||
|
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||||
"ToastCollectionItemsRemoveFailed": "Neuspješno brisanje stavke/-i iz kolekcije",
|
"ToastCollectionItemsRemoveFailed": "Neuspješno brisanje stavke/-i iz kolekcije",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Stavka/-e obrisane iz kolekcije",
|
"ToastCollectionItemsRemoveSuccess": "Stavka/-e obrisane iz kolekcije",
|
||||||
"ToastCollectionRemoveFailed": "Brisanje kolekcije neuspješno",
|
"ToastCollectionRemoveFailed": "Brisanje kolekcije neuspješno",
|
||||||
@@ -562,6 +594,8 @@
|
|||||||
"ToastLibraryScanStarted": "Sken biblioteke pokrenut",
|
"ToastLibraryScanStarted": "Sken biblioteke pokrenut",
|
||||||
"ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
|
"ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
|
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
@@ -572,18 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
||||||
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
|
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
|
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
||||||
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
||||||
"ToastUserDeleteSuccess": "Korisnik obrisan",
|
"ToastUserDeleteSuccess": "Korisnik obrisan"
|
||||||
"WeekdayFriday": "Petak",
|
|
||||||
"WeekdayMonday": "Ponedjeljak",
|
|
||||||
"WeekdaySaturday": "Subota",
|
|
||||||
"WeekdaySunday": "Nedjelja",
|
|
||||||
"WeekdayThursday": "Četvrtak",
|
|
||||||
"WeekdayTuesday": "Utorak",
|
|
||||||
"WeekdayWednesday": "Srijeda"
|
|
||||||
}
|
}
|
||||||
+59
-30
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Crea",
|
"ButtonCreate": "Crea",
|
||||||
"ButtonCreateBackup": "Crea un Backup",
|
"ButtonCreateBackup": "Crea un Backup",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Elimina",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Modifica Capitoli",
|
"ButtonEditChapters": "Modifica Capitoli",
|
||||||
"ButtonEditPodcast": "Modifica Podcast",
|
"ButtonEditPodcast": "Modifica Podcast",
|
||||||
"ButtonForceReScan": "Forza Re-Scan",
|
"ButtonForceReScan": "Forza Re-Scan",
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||||
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||||
"ButtonReScan": "Riscansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
"ButtonRestore": "Ripristina",
|
"ButtonRestore": "Ripristina",
|
||||||
"ButtonSave": "Salva",
|
"ButtonSave": "Salva",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"ButtonSearch": "Cerca",
|
"ButtonSearch": "Cerca",
|
||||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
|
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
|
||||||
"ButtonShiftTimes": "Ricerca veloce",
|
"ButtonShiftTimes": "Ricerca veloce",
|
||||||
"ButtonShow": "Mostra",
|
"ButtonShow": "Mostra",
|
||||||
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
||||||
@@ -74,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Carica Backup",
|
"ButtonUploadBackup": "Carica Backup",
|
||||||
"ButtonUploadCover": "Carica Cover",
|
"ButtonUploadCover": "Carica Cover",
|
||||||
"ButtonUploadOPMLFile": "Carica File OPML",
|
"ButtonUploadOPMLFile": "Carica File OPML",
|
||||||
|
"ButtonUserDelete": "Cancella Utente {0}",
|
||||||
|
"ButtonUserEdit": "Modifica Utente {0}",
|
||||||
"ButtonViewAll": "Mostra Tutto",
|
"ButtonViewAll": "Mostra Tutto",
|
||||||
"ButtonYes": "Si",
|
"ButtonYes": "Si",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -94,6 +98,7 @@
|
|||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
"HeaderIgnoredFiles": "File Ignorati",
|
"HeaderIgnoredFiles": "File Ignorati",
|
||||||
"HeaderItemFiles": "Files",
|
"HeaderItemFiles": "Files",
|
||||||
|
"HeaderItemMetadataUtils": "Utilità Metadata oggetti",
|
||||||
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
||||||
"HeaderLatestEpisodes": "Ultimi Episodi",
|
"HeaderLatestEpisodes": "Ultimi Episodi",
|
||||||
"HeaderLibraries": "Librerie",
|
"HeaderLibraries": "Librerie",
|
||||||
@@ -103,6 +108,9 @@
|
|||||||
"HeaderListeningStats": "Statistiche di Ascolto",
|
"HeaderListeningStats": "Statistiche di Ascolto",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Gestisci Generi",
|
||||||
|
"HeaderManageTags": "Gestisci Tags",
|
||||||
|
"HeaderMapDetails": "Mappa Dettagli",
|
||||||
"HeaderMatch": "Trova Corrispondenza",
|
"HeaderMatch": "Trova Corrispondenza",
|
||||||
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
||||||
"HeaderNewAccount": "Nuovo Account",
|
"HeaderNewAccount": "Nuovo Account",
|
||||||
@@ -113,7 +121,7 @@
|
|||||||
"HeaderPermissions": "Permessi",
|
"HeaderPermissions": "Permessi",
|
||||||
"HeaderPlayerQueue": "Coda Riproduzione",
|
"HeaderPlayerQueue": "Coda Riproduzione",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlist",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Elementi della playlist",
|
||||||
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
||||||
"HeaderPreviewCover": "Anteprima Cover",
|
"HeaderPreviewCover": "Anteprima Cover",
|
||||||
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
||||||
@@ -150,10 +158,11 @@
|
|||||||
"LabelAddedAt": "Aggiunto il",
|
"LabelAddedAt": "Aggiunto il",
|
||||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "aggiungi alla Playlist",
|
||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
|
"LabelAppend": "Appese",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||||
@@ -235,7 +244,7 @@
|
|||||||
"LabelLastSeen": "Ultimi Visti",
|
"LabelLastSeen": "Ultimi Visti",
|
||||||
"LabelLastTime": "Ultima Volta",
|
"LabelLastTime": "Ultima Volta",
|
||||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||||
"LabelLess": "Meno",
|
"LabelLess": "Poco",
|
||||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||||
"LabelLibrary": "Libreria",
|
"LabelLibrary": "Libreria",
|
||||||
"LabelLibraryItem": "Elementi della Library",
|
"LabelLibraryItem": "Elementi della Library",
|
||||||
@@ -246,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Allarme",
|
"LabelLogLevelWarn": "Allarme",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
||||||
"LabelMarkSeries": "Segna Serie",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Tipo Media",
|
"LabelMediaType": "Tipo Media",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -254,7 +262,7 @@
|
|||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Altro",
|
||||||
"LabelMissingParts": "Parti rimantenti",
|
"LabelMissingParts": "Parti rimantenti",
|
||||||
"LabelMore": "Espandi",
|
"LabelMore": "Molto",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
@@ -277,6 +285,7 @@
|
|||||||
"LabelNumberOfBooks": "Numero di libri",
|
"LabelNumberOfBooks": "Numero di libri",
|
||||||
"LabelNumberOfEpisodes": "# degli episodi",
|
"LabelNumberOfEpisodes": "# degli episodi",
|
||||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Percorso",
|
"LabelPath": "Percorso",
|
||||||
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
||||||
@@ -299,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -340,7 +350,7 @@
|
|||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta",
|
"LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Per prefisso si intende ad esempio \"il\" cone nel libro \"Il signore degli anelli\" che verrebbe ordinato come \"signore degli anelli, il\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Per prefisso si intende ad esempio \"il\" come nel libro \"Il signore degli anelli\" che verrebbe ordinato come \"signore degli anelli, il\"",
|
||||||
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
|
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
|
||||||
@@ -389,6 +399,9 @@
|
|||||||
"LabelTotalTimeListened": "Tempo totale di Ascolto",
|
"LabelTotalTimeListened": "Tempo totale di Ascolto",
|
||||||
"LabelTrackFromFilename": "Traccia da nome file",
|
"LabelTrackFromFilename": "Traccia da nome file",
|
||||||
"LabelTrackFromMetadata": "Traccia da Metadata",
|
"LabelTrackFromMetadata": "Traccia da Metadata",
|
||||||
|
"LabelTracks": "Traccia",
|
||||||
|
"LabelTracksMultiTrack": "Multi-traccia",
|
||||||
|
"LabelTracksSingleTrack": "Traccia-singola",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnknown": "Sconosciuto",
|
"LabelUnknown": "Sconosciuto",
|
||||||
"LabelUpdateCover": "Aggiornamento Cover",
|
"LabelUpdateCover": "Aggiornamento Cover",
|
||||||
@@ -411,27 +424,38 @@
|
|||||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||||
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
||||||
"LabelYourBookmarks": "I tuoi Preferiti",
|
"LabelYourBookmarks": "I tuoi Preferiti",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "le tue Playlist",
|
||||||
"LabelYourProgress": "Completato al",
|
"LabelYourProgress": "Completato al",
|
||||||
"MessageAddToPlayerQueue": "Add to player queue",
|
"MessageAddToPlayerQueue": "Aggiungi alla coda di riproduzione",
|
||||||
"MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoSeries": "You have no series",
|
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Il primo capitolo deve iniziare da 0",
|
||||||
|
"MessageChapterErrorStartGteDuration": "L'ora di inizio non valida deve essere inferiore alla durata dell'audiolibro",
|
||||||
|
"MessageChapterErrorStartLtPrev": "L'ora di inizio non valida deve essere maggiore o uguale all'ora di inizio del capitolo precedente",
|
||||||
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
||||||
"MessageCheckingCron": "Controllo cron...",
|
"MessageCheckingCron": "Controllo cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
||||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Avvertimento! Esiste già un genere simile con un nome simile \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFinished": "Incorporamento finito!",
|
"MessageEmbedFinished": "Incorporamento finito!",
|
||||||
@@ -442,6 +466,7 @@
|
|||||||
"MessageImportantNotice": "Avviso Importante!",
|
"MessageImportantNotice": "Avviso Importante!",
|
||||||
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
||||||
"MessageItemsSelected": "{0} oggetti Selezionati",
|
"MessageItemsSelected": "{0} oggetti Selezionati",
|
||||||
|
"MessageItemsUpdated": "{0} Oggetti aggiornati",
|
||||||
"MessageJoinUsOn": "Unisciti a noi su",
|
"MessageJoinUsOn": "Unisciti a noi su",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
||||||
"MessageLoading": "Caricamento...",
|
"MessageLoading": "Caricamento...",
|
||||||
@@ -474,25 +499,30 @@
|
|||||||
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
||||||
"MessageNoResults": "Nessun Risultato",
|
"MessageNoResults": "Nessun Risultato",
|
||||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||||
|
"MessageNoSeries": "Nessuna Serie",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
||||||
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
||||||
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
||||||
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
||||||
"MessageRemoveChapter": "Rimuovi Capitolo",
|
"MessageRemoveChapter": "Rimuovi Capitolo",
|
||||||
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Rimuovi dalla coda di riproduzione",
|
||||||
"MessageRemoveUserWarning": "Sei sicuro di voler eliminare definitivamente l'utente \"{0}\"?",
|
"MessageRemoveUserWarning": "Sei sicuro di voler eliminare definitivamente l'utente \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
||||||
|
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
||||||
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
||||||
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
||||||
"MessageSearchResultsFor": "cerca risultati per",
|
"MessageSearchResultsFor": "cerca risultati per",
|
||||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
|
||||||
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
||||||
"MessageThinking": "Elaborazione...",
|
"MessageThinking": "Elaborazione...",
|
||||||
"MessageUploaderItemFailed": "Caricamento Fallito",
|
"MessageUploaderItemFailed": "Caricamento Fallito",
|
||||||
@@ -514,7 +544,7 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
|
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
|
||||||
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
||||||
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
||||||
"PlaceholderSearch": "Cerca..",
|
"PlaceholderSearch": "Cerca..",
|
||||||
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
||||||
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
||||||
@@ -539,6 +569,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
||||||
"ToastBookmarkUpdateFailed": "Aggiornamento Segnalibro fallito",
|
"ToastBookmarkUpdateFailed": "Aggiornamento Segnalibro fallito",
|
||||||
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
||||||
|
"ToastChaptersHaveErrors": "I capitoli contengono errori",
|
||||||
|
"ToastChaptersMustHaveTitles": "I capitoli devono avere titoli",
|
||||||
"ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
|
"ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
|
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
|
||||||
"ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
|
"ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
|
||||||
@@ -562,28 +594,25 @@
|
|||||||
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
||||||
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
||||||
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistCreateFailed": "Errore Creazione playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistCreateSuccess": "Playlist creata",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistRemoveFailed": "Rimozione Playlist Fallita",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
|
||||||
|
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
||||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
|
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
||||||
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
"ToastSocketConnected": "Socket connesso",
|
"ToastSocketConnected": "Socket connesso",
|
||||||
"ToastSocketDisconnected": "Socket disconnesso",
|
"ToastSocketDisconnected": "Socket disconnesso",
|
||||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||||
"ToastUserDeleteSuccess": "Utente eliminato",
|
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||||
"WeekdayFriday": "Venerdì",
|
|
||||||
"WeekdayMonday": "Lunedì",
|
|
||||||
"WeekdaySaturday": "Sabato",
|
|
||||||
"WeekdaySunday": "Domenica",
|
|
||||||
"WeekdayThursday": "Giovedi",
|
|
||||||
"WeekdayTuesday": "Martedì",
|
|
||||||
"WeekdayWednesday": "Mercoledì"
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user