mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf071be247 | ||
|
|
6c05a0af8a | ||
|
|
0e292c64c4 | ||
|
|
725f8eecdb | ||
|
|
521a673094 | ||
|
|
d917f0e37d | ||
|
|
7ed5b1744f | ||
|
|
64a7cfac3b | ||
|
|
1ee7ba54f8 | ||
|
|
6bb18f8800 | ||
|
|
b26b854963 | ||
|
|
7d58361ced | ||
|
|
a3723f3d06 | ||
|
|
78d1cd0cfb | ||
|
|
d41366a417 | ||
|
|
a2347150a2 | ||
|
|
d33f23dede | ||
|
|
cfca2be1b2 | ||
|
|
73f07c1392 | ||
|
|
4541e9ddc3 | ||
|
|
972271a1a9 | ||
|
|
e97d92a8ac | ||
|
|
9a73e352d1 | ||
|
|
08f09f81fa | ||
|
|
c72609013c | ||
|
|
29a6434fdc | ||
|
|
eb2ea9950a | ||
|
|
e307ded192 | ||
|
|
2d6c997b38 | ||
|
|
232a80a848 | ||
|
|
083f8faa46 | ||
|
|
0fcf978ffe | ||
|
|
c1360267c6 | ||
|
|
084bea6b15 | ||
|
|
2032dd88ba | ||
|
|
b11b1be432 | ||
|
|
b743b34fab | ||
|
|
950d10091d | ||
|
|
af0e02b9a2 | ||
|
|
1332147c4a | ||
|
|
f07cb1e7a3 | ||
|
|
53dbdd115f | ||
|
|
a217ed5574 | ||
|
|
531f947754 | ||
|
|
c957e9483e | ||
|
|
623a706555 | ||
|
|
7e171576e0 | ||
|
|
0979b3e03d | ||
|
|
1131bfa751 | ||
|
|
f9b87b94bf | ||
|
|
59ed2ec87f | ||
|
|
7b0b79e3a1 | ||
|
|
53f73e1201 | ||
|
|
c62a1dfff0 | ||
|
|
61f8055493 | ||
|
|
000d7fd249 | ||
|
|
087de03a1f | ||
|
|
a3ca6159fb | ||
|
|
5de6ee136a | ||
|
|
d5a19f2b42 | ||
|
|
e3ec5dd506 | ||
|
|
762748225d | ||
|
|
4db34e0c56 | ||
|
|
fb078d05bc | ||
|
|
f59edffa43 | ||
|
|
7aa0ddb71f | ||
|
|
a0a6256c7a | ||
|
|
df7e331605 | ||
|
|
8c23704e17 | ||
|
|
12abb1731c | ||
|
|
180293ebc1 | ||
|
|
e2af33e136 | ||
|
|
42e68edc65 | ||
|
|
47e732c213 | ||
|
|
77a86d92f4 | ||
|
|
64a8a046c1 | ||
|
|
1f02cbddd3 | ||
|
|
5e7bca02b3 | ||
|
|
097f9549b1 | ||
|
|
45434b16e0 | ||
|
|
6af5ac2be1 | ||
|
|
34ff7efa27 | ||
|
|
8f4391003f | ||
|
|
ecefb30f3d | ||
|
|
a8162b57ba | ||
|
|
b0edac4234 | ||
|
|
98c4045a71 | ||
|
|
24e90e2ead | ||
|
|
145e0217b6 | ||
|
|
e5925fb1b6 | ||
|
|
9e416d02bd | ||
|
|
82b7068130 | ||
|
|
579ee36857 | ||
|
|
4f2d7a519d | ||
|
|
a3642d92c5 | ||
|
|
224f36164f | ||
|
|
638c220ae8 | ||
|
|
51070b3e7b | ||
|
|
0aa2723063 | ||
|
|
1af66c8e8b | ||
|
|
7df8795d52 | ||
|
|
a0e9ae7092 | ||
|
|
0f0d8e317a | ||
|
|
3d5ca7d5c4 | ||
|
|
e33104fa2b | ||
|
|
a2f1723642 | ||
|
|
93357cf280 | ||
|
|
767427c787 | ||
|
|
9377631896 | ||
|
|
d08af094b8 | ||
|
|
c307b1e6fb | ||
|
|
d387d5b758 | ||
|
|
c285dd666d | ||
|
|
b37b382ea7 | ||
|
|
a2cd755ffa | ||
|
|
340aedfe13 | ||
|
|
6fafa7a75e | ||
|
|
03df5aaf42 | ||
|
|
6d84db08a8 | ||
|
|
1a5e0d2a5e | ||
|
|
70d887bada | ||
|
|
ee0ac00f80 | ||
|
|
fdfb07ff2c | ||
|
|
b648155170 | ||
|
|
59dc5299b1 | ||
|
|
357a63a4d9 | ||
|
|
94912c7542 | ||
|
|
fae182b328 | ||
|
|
9ba2f3e33a | ||
|
|
5ca2bc5d64 | ||
|
|
442687b198 | ||
|
|
7e400d3e9c | ||
|
|
e3ba739db5 | ||
|
|
cd92a22f4d | ||
|
|
d24ed98bcd | ||
|
|
bcd224f534 | ||
|
|
1a93103e50 | ||
|
|
45ccf9d4be | ||
|
|
052a8307b3 | ||
|
|
a0c0b9ea76 | ||
|
|
7485cf1a26 | ||
|
|
8931702f1b | ||
|
|
00fae3eb16 | ||
|
|
003e8e17be | ||
|
|
edd9443d51 | ||
|
|
b93a4c6792 | ||
|
|
30cf144090 | ||
|
|
f17abef20a | ||
|
|
937438800e | ||
|
|
892fb6410c | ||
|
|
7008267e42 | ||
|
|
2e5e02472c | ||
|
|
f9d37228cf | ||
|
|
f48d52a489 | ||
|
|
7d8c8fa5bb | ||
|
|
96a739e22d | ||
|
|
c3ec036009 | ||
|
|
c7794e00f6 | ||
|
|
3316394f5c | ||
|
|
c5d66989a6 | ||
|
|
e6b886a511 | ||
|
|
9bdfb05ea6 | ||
|
|
52d02b32f7 | ||
|
|
adff5a7705 | ||
|
|
60fb4090ff | ||
|
|
dd28be0113 | ||
|
|
5a60bb8267 | ||
|
|
2749b710e6 | ||
|
|
55ddcde631 | ||
|
|
4d2bcfd167 |
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/pJsjuNCKRq
|
||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||
- name: Matrix
|
||||
url: https://matrix.to/#/#audiobookshelf:matrix.org
|
||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||
@@ -26,7 +26,7 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||
.material-icons:not([class*="text-"]) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,10 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||
.material-icons-outlined:not([class*="text-"]) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Gentium Book Basic';
|
||||
font-style: normal;
|
||||
|
||||
@@ -20,42 +20,67 @@
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.slide-enter-to, .slide-leave {
|
||||
.slide-enter-to,
|
||||
.slide-leave {
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-enter, .slide-leave-to {
|
||||
.slide-enter,
|
||||
.slide-leave-to {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-enter, .menu-leave-active {
|
||||
.menu-enter,
|
||||
.menu-leave-active {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
|
||||
.menu-enter-active {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-leave-active {
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.menu-enter,
|
||||
.menu-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menux-enter, .menux-leave-active {
|
||||
.menux-enter,
|
||||
.menux-leave-active {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
.menux-enter-active {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menux-leave-active {
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.menux-enter,
|
||||
.menux-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.list-complete-item {
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.list-complete-enter-from,
|
||||
.list-complete-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.list-complete-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -18,22 +18,22 @@
|
||||
<widgets-notification-widget class="hidden md:block" />
|
||||
|
||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
||||
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||
</ui-tooltip>
|
||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
</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">
|
||||
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
|
||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||
</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">
|
||||
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
|
||||
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||
</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">
|
||||
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
|
||||
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||
</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">
|
||||
@@ -41,13 +41,17 @@
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
</span>
|
||||
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
||||
<span class="material-icons text-gray-100">person</span>
|
||||
<span class="material-icons text-xl text-gray-100">person</span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||
<h1 class="text-2xl px-4">{{ $getString('MessageItemsSelected', [numLibraryItemsSelected]) }}</h1>
|
||||
<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', [numMediaItemsSelected]) }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<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>
|
||||
{{ $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
@@ -57,16 +61,16 @@
|
||||
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
||||
<template v-if="userCanUpdate">
|
||||
<ui-tooltip text="Edit" direction="bottom">
|
||||
<ui-icon-btn v-show="!processingBatchDelete" 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>
|
||||
</template>
|
||||
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
</ui-tooltip>
|
||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
|
||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,9 +81,7 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processingBatchDelete: false,
|
||||
totalEntities: 0,
|
||||
isAllSelected: false
|
||||
totalEntities: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -107,11 +109,14 @@ export default {
|
||||
username() {
|
||||
return this.user ? this.user.username : 'err'
|
||||
},
|
||||
numLibraryItemsSelected() {
|
||||
return this.selectedLibraryItems.length
|
||||
numMediaItemsSelected() {
|
||||
return this.selectedMediaItems.length
|
||||
},
|
||||
selectedLibraryItems() {
|
||||
return this.$store.state.selectedLibraryItems
|
||||
selectedMediaItems() {
|
||||
return this.$store.state.globals.selectedMediaItems
|
||||
},
|
||||
selectedMediaItemsArePlayable() {
|
||||
return !this.selectedMediaItems.some(i => !i.hasTracks)
|
||||
},
|
||||
userMediaProgress() {
|
||||
return this.$store.state.user.user.mediaProgress || []
|
||||
@@ -127,8 +132,8 @@ export default {
|
||||
},
|
||||
selectedIsFinished() {
|
||||
// Find an item that is not finished, if none then all items finished
|
||||
return !this.selectedLibraryItems.find((libraryItemId) => {
|
||||
var itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === libraryItemId)
|
||||
return !this.selectedMediaItems.find((item) => {
|
||||
const itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === item.id)
|
||||
return !itemProgress || !itemProgress.isFinished
|
||||
})
|
||||
},
|
||||
@@ -149,18 +154,55 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelSelectionMode() {
|
||||
if (this.processingBatchDelete) return
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
async playSelectedItems() {
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
|
||||
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
||||
const libraryItems = await this.$axios.$post(`/api/items/batch/get`, { libraryItemIds }).catch((error) => {
|
||||
const errorMsg = error.response.data || 'Failed to get items'
|
||||
console.error(errorMsg, error)
|
||||
this.$toast.error(errorMsg)
|
||||
return []
|
||||
})
|
||||
|
||||
if (!libraryItems.length) {
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
return
|
||||
}
|
||||
|
||||
const queueItems = []
|
||||
libraryItems.forEach((item) => {
|
||||
queueItems.push({
|
||||
libraryItemId: item.id,
|
||||
libraryId: item.libraryId,
|
||||
episodeId: null,
|
||||
title: item.media.metadata.title,
|
||||
subtitle: item.media.metadata.authors.map((au) => au.name).join(', '),
|
||||
caption: '',
|
||||
duration: item.media.duration || null,
|
||||
coverPath: item.media.coverPath || null
|
||||
})
|
||||
})
|
||||
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: queueItems[0].libraryItemId,
|
||||
queueItems
|
||||
})
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
},
|
||||
cancelSelectionMode() {
|
||||
if (this.processingBatch) return
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
this.isAllSelected = false
|
||||
},
|
||||
toggleBatchRead() {
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
var newIsFinished = !this.selectedIsFinished
|
||||
var updateProgressPayloads = this.selectedLibraryItems.map((lid) => {
|
||||
const newIsFinished = !this.selectedIsFinished
|
||||
const updateProgressPayloads = this.selectedMediaItems.map((item) => {
|
||||
return {
|
||||
libraryItemId: lid,
|
||||
libraryItemId: item.id,
|
||||
isFinished: newIsFinished
|
||||
}
|
||||
})
|
||||
@@ -170,7 +212,7 @@ export default {
|
||||
.then(() => {
|
||||
this.$toast.success('Batch update success!')
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -180,26 +222,23 @@ export default {
|
||||
})
|
||||
},
|
||||
batchDeleteClick() {
|
||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} 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 audiobookText = this.numMediaItemsSelected > 1 ? `these ${this.numMediaItemsSelected} items` : 'this item'
|
||||
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)) {
|
||||
this.processingBatchDelete = true
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
this.$axios
|
||||
.$post(`/api/items/batch/delete`, {
|
||||
libraryItemIds: this.selectedLibraryItems
|
||||
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success('Batch delete success!')
|
||||
this.processingBatchDelete = false
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch delete failed')
|
||||
console.error('Failed to batch delete', error)
|
||||
this.processingBatchDelete = false
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ export default {
|
||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||
return this.bookCoverWidth / baseSize
|
||||
},
|
||||
selectedLibraryItems() {
|
||||
return this.$store.state.selectedLibraryItems || []
|
||||
selectedMediaItems() {
|
||||
return this.$store.state.globals.selectedMediaItems || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -100,15 +100,15 @@ export default {
|
||||
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
||||
|
||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
||||
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||
this.lastItemIndexSelected = indexOf
|
||||
} else {
|
||||
this.lastItemIndexSelected = -1
|
||||
}
|
||||
|
||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||
var loopStart = indexOf
|
||||
var loopEnd = lastLastItemIndexSelected
|
||||
let loopStart = indexOf
|
||||
let loopEnd = lastLastItemIndexSelected
|
||||
if (indexOf > lastLastItemIndexSelected) {
|
||||
loopStart = lastLastItemIndexSelected
|
||||
loopEnd = indexOf
|
||||
@@ -117,12 +117,12 @@ export default {
|
||||
const flattenedEntitiesArray = []
|
||||
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
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = flattenedEntitiesArray[i]
|
||||
if (thisEntity) {
|
||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
||||
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||
isSelecting = true
|
||||
break
|
||||
}
|
||||
@@ -133,13 +133,23 @@ export default {
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = flattenedEntitiesArray[i]
|
||||
if (thisEntity) {
|
||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
||||
const mediaItem = {
|
||||
id: thisEntity.id,
|
||||
mediaType: thisEntity.mediaType,
|
||||
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||
}
|
||||
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||
} else {
|
||||
console.error('Invalid entity index', i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
||||
const mediaItem = {
|
||||
id: entity.id,
|
||||
mediaType: entity.mediaType,
|
||||
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||
}
|
||||
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -119,14 +119,14 @@ export default {
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
},
|
||||
updateSelectionMode(val) {
|
||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
||||
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
||||
this.shelf.entities.forEach((ent) => {
|
||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||
if (!component || !component.length) return
|
||||
component = component[0]
|
||||
component.setSelectionMode(val)
|
||||
component.selected = selectedLibraryItems.includes(ent.id)
|
||||
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||
})
|
||||
} else if (this.shelf.type === 'episode') {
|
||||
this.shelf.entities.forEach((ent) => {
|
||||
@@ -134,7 +134,7 @@ export default {
|
||||
if (!component || !component.length) return
|
||||
component = component[0]
|
||||
component.setSelectionMode(val)
|
||||
component.selected = selectedLibraryItems.includes(ent.id)
|
||||
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,63 +2,91 @@
|
||||
<div class="w-full h-20 md:h-10 relative">
|
||||
<div class="flex md:hidden h-10 items-center">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonHome }}</p>
|
||||
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
||||
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<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>
|
||||
</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'">
|
||||
<p 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">
|
||||
<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>
|
||||
</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'">
|
||||
<p 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>
|
||||
</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'">
|
||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
||||
</nuxt-link>
|
||||
</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">
|
||||
<template v-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||
<p v-if="!selectedSeries" class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||
<div v-else class="items-center hidden md:flex w-full">
|
||||
<p class="pl-2 font-book text-lg">
|
||||
{{ seriesName }}
|
||||
</p>
|
||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||
<span class="font-mono">{{ numShowing }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4" @click="markSeriesFinished">
|
||||
<div class="h-5 w-5">
|
||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||
<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" />
|
||||
</svg>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
|
||||
</ui-btn>
|
||||
<!-- Series books page -->
|
||||
<template v-if="selectedSeries">
|
||||
<p class="pl-2 font-book text-base md:text-lg">
|
||||
{{ seriesName }}
|
||||
</p>
|
||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||
<span class="font-mono">{{ numShowing }}</span>
|
||||
</div>
|
||||
<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-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">
|
||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||
<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" />
|
||||
</svg>
|
||||
<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" />
|
||||
</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>
|
||||
<!-- library & collections page -->
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||
<p class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
|
||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" 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" 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" 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" 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-sort-select v-if="isSeriesPage" 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" />
|
||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||
<controls-library-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-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" />
|
||||
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete" :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>
|
||||
<!-- search page -->
|
||||
<template v-else-if="page === 'search'">
|
||||
<div class="flex-grow" />
|
||||
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
||||
<div class="flex-grow" />
|
||||
</template>
|
||||
<!-- authors page -->
|
||||
<template v-else-if="page === 'authors'">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,28 +114,30 @@ export default {
|
||||
totalEntities: 0,
|
||||
processingSeries: false,
|
||||
processingIssues: false,
|
||||
processingAuthors: false,
|
||||
seriesSortItems: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: 'Number of Books',
|
||||
value: 'numBooks'
|
||||
},
|
||||
{
|
||||
text: 'Date Added',
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Total Duration',
|
||||
value: 'totalDuration'
|
||||
}
|
||||
]
|
||||
processingAuthors: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seriesSortItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelName,
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNumberOfBooks,
|
||||
value: 'numBooks'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAddedAt,
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTotalDuration,
|
||||
value: 'totalDuration'
|
||||
}
|
||||
]
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
@@ -135,12 +165,21 @@ export default {
|
||||
isCollectionsPage() {
|
||||
return this.page === 'collections'
|
||||
},
|
||||
isPlaylistsPage() {
|
||||
return this.page === 'playlists'
|
||||
},
|
||||
isHomePage() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
isPodcastSearchPage() {
|
||||
return this.$route.name === 'library-library-podcast-search'
|
||||
},
|
||||
isPodcastLatestPage() {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
},
|
||||
numShowing() {
|
||||
return this.totalEntities
|
||||
},
|
||||
@@ -149,8 +188,12 @@ export default {
|
||||
if (!this.page) return this.$strings.LabelBooks
|
||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||
return ''
|
||||
},
|
||||
seriesId() {
|
||||
return this.selectedSeries ? this.selectedSeries.id : null
|
||||
},
|
||||
seriesName() {
|
||||
return this.selectedSeries ? this.selectedSeries.name : null
|
||||
},
|
||||
@@ -161,9 +204,16 @@ export default {
|
||||
if (!this.seriesProgress) return []
|
||||
return this.seriesProgress.libraryItemIds || []
|
||||
},
|
||||
isBatchSelecting() {
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
},
|
||||
isSeriesFinished() {
|
||||
return this.seriesProgress && !!this.seriesProgress.isFinished
|
||||
},
|
||||
isSeriesRemovedFromContinueListening() {
|
||||
if (!this.seriesId) return false
|
||||
return this.$store.getters['user/getIsSeriesRemovedFromContinueListening'](this.seriesId)
|
||||
},
|
||||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||
},
|
||||
@@ -196,6 +246,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reAddSeriesToContinueListening() {
|
||||
this.processingSeries = true
|
||||
this.$axios
|
||||
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||
.then(() => {
|
||||
this.$toast.success('Series re-added to continue listening')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to re-add series to continue listening', error)
|
||||
this.$toast.error('Failed to re-add series to continue listening')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingSeries = false
|
||||
})
|
||||
},
|
||||
async matchAllAuthors() {
|
||||
this.processingAuthors = true
|
||||
|
||||
@@ -233,12 +298,13 @@ export default {
|
||||
.then(() => {
|
||||
this.$toast.success('Removed library items with issues')
|
||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
this.processingIssues = false
|
||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove library items with issues', error)
|
||||
this.$toast.error('Failed to remove library items with issues')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingIssues = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
</template>
|
||||
|
||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" 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">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||
<div v-if="userIsAdminOrUp" class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||
<!-- Clear filter only available on Library bookshelf -->
|
||||
<div v-if="entityName === 'books'" class="flex justify-center mt-2">
|
||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">Clear Filter</ui-btn>
|
||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,14 +85,15 @@ export default {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||
},
|
||||
emptyMessage() {
|
||||
if (this.page === 'series') return 'You have no series'
|
||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||
if (this.hasFilter) {
|
||||
if (this.filterName === 'Issues') return 'No Issues'
|
||||
else if (this.filterName === 'Feed-open') return 'No RSS feeds are open'
|
||||
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||
return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue])
|
||||
}
|
||||
return 'No results'
|
||||
return this.$strings.MessageNoResults
|
||||
},
|
||||
entityName() {
|
||||
if (!this.page) return 'books'
|
||||
@@ -166,7 +167,7 @@ export default {
|
||||
return coverSize
|
||||
},
|
||||
bookHeight() {
|
||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
||||
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
||||
return this.bookWidth * 1.6
|
||||
},
|
||||
shelfPadding() {
|
||||
@@ -200,8 +201,8 @@ export default {
|
||||
// Includes margin
|
||||
return this.entityWidth + 24
|
||||
},
|
||||
selectedLibraryItems() {
|
||||
return this.$store.state.selectedLibraryItems || []
|
||||
selectedMediaItems() {
|
||||
return this.$store.state.globals.selectedMediaItems || []
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||
@@ -219,6 +220,8 @@ export default {
|
||||
this.$store.commit('showEditModal', entity)
|
||||
} else if (this.entityName === 'collections') {
|
||||
this.$store.commit('globals/setEditCollection', entity)
|
||||
} else if (this.entityName === 'playlists') {
|
||||
this.$store.commit('globals/setEditPlaylist', entity)
|
||||
}
|
||||
},
|
||||
clearSelectedEntities() {
|
||||
@@ -229,7 +232,7 @@ export default {
|
||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
||||
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||
this.lastItemIndexSelected = indexOf
|
||||
} else {
|
||||
this.lastItemIndexSelected = -1
|
||||
@@ -248,7 +251,7 @@ export default {
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = this.entities[i]
|
||||
if (thisEntity && !thisEntity.collapsedSeries) {
|
||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
||||
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||
isSelecting = true
|
||||
break
|
||||
}
|
||||
@@ -266,16 +269,27 @@ export default {
|
||||
const entityComponentRef = this.entityComponentRefs[i]
|
||||
if (thisEntity && entityComponentRef) {
|
||||
entityComponentRef.selected = isSelecting
|
||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
||||
|
||||
const mediaItem = {
|
||||
id: thisEntity.id,
|
||||
mediaType: thisEntity.mediaType,
|
||||
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||
}
|
||||
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||
} else {
|
||||
console.error('Invalid entity index', i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
||||
const mediaItem = {
|
||||
id: entity.id,
|
||||
mediaType: entity.mediaType,
|
||||
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||
}
|
||||
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||
}
|
||||
|
||||
var newIsSelectionMode = !!this.selectedLibraryItems.length
|
||||
const newIsSelectionMode = !!this.selectedMediaItems.length
|
||||
if (this.isSelectionMode !== newIsSelectionMode) {
|
||||
this.isSelectionMode = newIsSelectionMode
|
||||
this.updateBookSelectionMode(newIsSelectionMode)
|
||||
@@ -301,11 +315,11 @@ export default {
|
||||
this.currentSFQueryString = this.buildSearchParams()
|
||||
}
|
||||
|
||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
|
||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||
console.error('failed to fetch books', error)
|
||||
return null
|
||||
})
|
||||
@@ -560,6 +574,33 @@ export default {
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
playlistAdded(playlist) {
|
||||
if (this.entityName !== 'playlists') return
|
||||
console.log(`[LazyBookshelf] playlistAdded ${playlist.id}`, playlist)
|
||||
this.resetEntities()
|
||||
},
|
||||
playlistUpdated(playlist) {
|
||||
if (this.entityName !== 'playlists') return
|
||||
console.log(`[LazyBookshelf] playlistUpdated ${playlist.id}`, playlist)
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities[indexOf] = playlist
|
||||
if (this.entityComponentRefs[indexOf]) {
|
||||
this.entityComponentRefs[indexOf].setEntity(playlist)
|
||||
}
|
||||
}
|
||||
},
|
||||
playlistRemoved(playlist) {
|
||||
if (this.entityName !== 'playlists') return
|
||||
console.log(`[LazyBookshelf] playlistRemoved ${playlist.id}`, playlist)
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities = this.entities.filter((ent) => ent.id !== playlist.id)
|
||||
this.totalEntities--
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
initSizeData(_bookshelf) {
|
||||
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
||||
if (!bookshelf) {
|
||||
@@ -640,6 +681,9 @@ export default {
|
||||
this.$root.socket.on('collection_added', this.collectionAdded)
|
||||
this.$root.socket.on('collection_updated', this.collectionUpdated)
|
||||
this.$root.socket.on('collection_removed', this.collectionRemoved)
|
||||
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||
} else {
|
||||
console.error('Bookshelf - Socket not initialized')
|
||||
}
|
||||
@@ -666,6 +710,9 @@ export default {
|
||||
this.$root.socket.off('collection_added', this.collectionAdded)
|
||||
this.$root.socket.off('collection_updated', this.collectionUpdated)
|
||||
this.$root.socket.off('collection_removed', this.collectionRemoved)
|
||||
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||
} else {
|
||||
console.error('Bookshelf - Socket not initialized')
|
||||
}
|
||||
|
||||
48
client/components/app/SettingsContent.vue
Normal file
48
client/components/app/SettingsContent.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ headerText }}</h1>
|
||||
|
||||
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" />
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerText: String,
|
||||
description: String,
|
||||
note: String,
|
||||
showAddButton: Boolean
|
||||
},
|
||||
methods: {
|
||||
clicked() {
|
||||
this.$emit('clicked')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#settings-description a {
|
||||
color: rgb(96 165 250);
|
||||
}
|
||||
#settings-description a:hover {
|
||||
color: rgb(147 197 253);
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
#settings-description code {
|
||||
font-size: 0.875rem;
|
||||
border-radius: 6px;
|
||||
background-color: rgb(82, 82, 82);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons">format_list_bulleted</span>
|
||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||
|
||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</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'">
|
||||
<span class="material-icons-outlined">collections_bookmark</span>
|
||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||
|
||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||
|
||||
@@ -71,6 +71,14 @@
|
||||
<div v-show="isPodcastSearchPage" 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'">
|
||||
<span class="material-icons text-2.5xl">queue_music</span>
|
||||
|
||||
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||
<span class="material-icons text-2xl">warning</span>
|
||||
|
||||
@@ -143,6 +151,9 @@ export default {
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
},
|
||||
isPlaylistsPage() {
|
||||
return this.paramId === 'playlists'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
@@ -173,6 +184,9 @@ export default {
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
showPlaylists() {
|
||||
return this.$store.state.libraries.numUserPlaylists > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<player-ui
|
||||
ref="audioPlayer"
|
||||
@@ -297,6 +299,16 @@ export default {
|
||||
this.playerHandler.seek(e.seekTime)
|
||||
}
|
||||
},
|
||||
mediaSessionPreviousTrack() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.prevChapter()
|
||||
}
|
||||
},
|
||||
mediaSessionNextTrack() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.nextChapter()
|
||||
}
|
||||
},
|
||||
updateMediaSessionPlaybackState() {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
||||
@@ -330,8 +342,9 @@ export default {
|
||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||
// navigator.mediaSession.setActionHandler('previoustrack')
|
||||
// navigator.mediaSession.setActionHandler('nexttrack')
|
||||
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionPreviousTrack)
|
||||
const hasNextChapter = this.$refs.audioPlayer && this.$refs.audioPlayer.hasNextChapter
|
||||
navigator.mediaSession.setActionHandler('nexttrack', hasNextChapter ? this.mediaSessionNextTrack : null)
|
||||
} else {
|
||||
console.warn('Media session not available')
|
||||
}
|
||||
@@ -365,7 +378,7 @@ export default {
|
||||
}
|
||||
},
|
||||
streamReady() {
|
||||
console.log(`[STREAM-CONTAINER] Stream Ready`)
|
||||
console.log(`[StreamContainer] Stream Ready`)
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setStreamReady()
|
||||
} else {
|
||||
|
||||
@@ -410,6 +410,10 @@ export default {
|
||||
{
|
||||
func: 'toggleFinished',
|
||||
text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
|
||||
},
|
||||
{
|
||||
func: 'openPlaylists',
|
||||
text: this.$strings.LabelAddToPlaylist
|
||||
}
|
||||
]
|
||||
if (this.continueListeningShelf) {
|
||||
@@ -448,6 +452,12 @@ export default {
|
||||
text: this.$strings.LabelAddToCollection
|
||||
})
|
||||
}
|
||||
if (this.numTracks) {
|
||||
items.push({
|
||||
func: 'openPlaylists',
|
||||
text: this.$strings.LabelAddToPlaylist
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this.userCanUpdate) {
|
||||
items.push({
|
||||
@@ -739,6 +749,10 @@ export default {
|
||||
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.store.commit('globals/setShowCollectionsModal', true)
|
||||
},
|
||||
openPlaylists() {
|
||||
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
|
||||
this.store.commit('globals/setShowPlaylistsModal', true)
|
||||
},
|
||||
createMoreMenu() {
|
||||
if (!this.$refs.moreIcon) return
|
||||
|
||||
|
||||
115
client/components/cards/LazyPlaylistCard.vue
Normal file
115
client/components/cards/LazyPlaylistCard.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||
</div>
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number,
|
||||
bookshelfView: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
playlistMount: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
playlist: null,
|
||||
isSelectionMode: false,
|
||||
selected: false,
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.875
|
||||
},
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||
return this.width / 240
|
||||
},
|
||||
title() {
|
||||
return this.playlist ? this.playlist.name : ''
|
||||
},
|
||||
items() {
|
||||
return this.playlist ? this.playlist.items || [] : []
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.store.state.libraries.currentLibraryId
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
const constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.store.getters['user/getUserCanUpdate']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setEntity(playlist) {
|
||||
this.playlist = playlist
|
||||
},
|
||||
setSelectionMode(val) {
|
||||
this.isSelectionMode = val
|
||||
},
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickCard() {
|
||||
if (!this.playlist) return
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
router.push(`/playlist/${this.playlist.id}`)
|
||||
},
|
||||
clickEdit() {
|
||||
this.$emit('edit', this.playlist)
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.playlistMount) {
|
||||
this.setEntity(this.playlistMount)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||
</div>
|
||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_right</span>
|
||||
<span class="material-icons text-2xl">arrow_right</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@@ -30,7 +30,7 @@
|
||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_left</span>
|
||||
<span class="material-icons text-2xl">arrow_left</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate">Back</span>
|
||||
@@ -41,9 +41,9 @@
|
||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
@@ -67,120 +67,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
sublist: null,
|
||||
seriesItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Series Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
}
|
||||
],
|
||||
bookItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Series',
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Missing',
|
||||
value: 'missing',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
},
|
||||
{
|
||||
text: 'RSS Feed Open',
|
||||
value: 'feed-open',
|
||||
sublist: false
|
||||
}
|
||||
],
|
||||
podcastItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
sublist: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -203,6 +90,130 @@ export default {
|
||||
isPodcast() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||
},
|
||||
seriesItems() {
|
||||
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.LabelAuthor,
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNarrator,
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSeriesProgress,
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
}
|
||||
]
|
||||
},
|
||||
bookItems() {
|
||||
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.LabelSeries,
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAuthor,
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNarrator,
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelProgress,
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelMissing,
|
||||
value: 'missing',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTracks,
|
||||
value: 'tracks',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.ButtonIssues,
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRSSFeedOpen,
|
||||
value: 'feed-open',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
},
|
||||
podcastItems() {
|
||||
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() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
if (this.isPodcast) return this.podcastItems
|
||||
@@ -257,10 +268,88 @@ export default {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
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() {
|
||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
|
||||
@@ -56,31 +56,31 @@ export default {
|
||||
podcastItems() {
|
||||
return [
|
||||
{
|
||||
text: 'Title',
|
||||
text: this.$strings.LabelTitle,
|
||||
value: 'media.metadata.title'
|
||||
},
|
||||
{
|
||||
text: 'Author',
|
||||
text: this.$strings.LabelAuthor,
|
||||
value: 'media.metadata.author'
|
||||
},
|
||||
{
|
||||
text: 'Added At',
|
||||
text: this.$strings.LabelAddedAt,
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Size',
|
||||
text: this.$strings.LabelSize,
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: '# of Episodes',
|
||||
text: this.$strings.LabelNumberOfEpisodes,
|
||||
value: 'media.numTracks'
|
||||
},
|
||||
{
|
||||
text: 'File Birthtime',
|
||||
text: this.$strings.LabelFileBirthtime,
|
||||
value: 'birthtimeMs'
|
||||
},
|
||||
{
|
||||
text: 'File Modified',
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
}
|
||||
]
|
||||
@@ -92,35 +92,35 @@ export default {
|
||||
value: 'media.metadata.title'
|
||||
},
|
||||
{
|
||||
text: 'Author (First Last)',
|
||||
text: this.$strings.LabelAuthorFirstLast,
|
||||
value: 'media.metadata.authorName'
|
||||
},
|
||||
{
|
||||
text: 'Author (Last, First)',
|
||||
text: this.$strings.LabelAuthorLastFirst,
|
||||
value: 'media.metadata.authorNameLF'
|
||||
},
|
||||
{
|
||||
text: 'Published Year',
|
||||
text: this.$strings.LabelPublishYear,
|
||||
value: 'media.metadata.publishedYear'
|
||||
},
|
||||
{
|
||||
text: 'Added At',
|
||||
text: this.$strings.LabelAddedAt,
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Size',
|
||||
text: this.$strings.LabelSize,
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
text: this.$strings.LabelDuration,
|
||||
value: 'media.duration'
|
||||
},
|
||||
{
|
||||
text: 'File Birthtime',
|
||||
text: this.$strings.LabelFileBirthtime,
|
||||
value: 'birthtimeMs'
|
||||
},
|
||||
{
|
||||
text: 'File Modified',
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@ export default {
|
||||
return [
|
||||
...this.bookItems,
|
||||
{
|
||||
text: 'Sequence',
|
||||
text: this.$strings.LabelSequence,
|
||||
value: 'sequence'
|
||||
}
|
||||
]
|
||||
|
||||
51
client/components/covers/PlaylistCover.vue
Normal file
51
client/components/covers/PlaylistCover.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
width: Number,
|
||||
height: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
return this.width / (120 * 1.6 * 2)
|
||||
},
|
||||
itemCoverWidth() {
|
||||
if (this.libraryItemCovers.length === 1) return this.width
|
||||
return this.width / 2
|
||||
},
|
||||
libraryItemCovers() {
|
||||
if (!this.items.length) return []
|
||||
if (this.items.length === 1) return [this.items[0].libraryItem]
|
||||
|
||||
const covers = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let index = i % this.items.length
|
||||
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
|
||||
|
||||
covers.push(this.items[index].libraryItem)
|
||||
}
|
||||
return covers
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@
|
||||
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelUpdateCover }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelUpdateDetails }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
return this.$store.state.globals.showBatchQuickMatchModal
|
||||
},
|
||||
selectedBookIds() {
|
||||
return this.$store.state.selectedLibraryItems || []
|
||||
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="flex-grow px-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">add</span></ui-btn>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">add</span></ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -136,6 +136,7 @@ export default {
|
||||
})
|
||||
if (result && result.updated) {
|
||||
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
||||
this.$store.commit('globals/showEditAuthorModal', result.author)
|
||||
}
|
||||
this.processing = false
|
||||
},
|
||||
@@ -157,7 +158,10 @@ export default {
|
||||
if (!response) {
|
||||
this.$toast.error('Author not found')
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
if (response.author.imagePath) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
this.$store.commit('globals/showEditAuthorModal', response.author)
|
||||
}
|
||||
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
} else {
|
||||
this.$toast.info('No updates were made for Author')
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex-grow pr-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">forward</span></ui-btn>
|
||||
<div class="pl-2 flex items-center">
|
||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<transition-group name="list-complete" tag="div">
|
||||
<template v-for="collection in sortedCollections">
|
||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||
<modals-collections-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
return this.$store.state.globals.showBatchCollectionModal
|
||||
},
|
||||
selectedBookIds() {
|
||||
return this.$store.state.selectedLibraryItems || []
|
||||
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
@@ -112,23 +112,21 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
loadCollections() {
|
||||
if (!this.collections.length) {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
||||
.then((data) => {
|
||||
if (data.results) {
|
||||
this.$store.commit('libraries/setCollections', data.results || [])
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get collections', error)
|
||||
this.$toast.error('Failed to load collections')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
||||
.then((data) => {
|
||||
if (data.results) {
|
||||
this.$store.commit('libraries/setCollections', data.results || [])
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get collections', error)
|
||||
this.$toast.error('Failed to load collections')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
removeFromCollection(collection) {
|
||||
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
||||
@@ -231,19 +229,3 @@ export default {
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.list-complete-item {
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.list-complete-enter-from,
|
||||
.list-complete-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.list-complete-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
58
client/components/modals/collections/CollectionItem.vue
Normal file
58
client/components/modals/collections/CollectionItem.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-20 max-w-20 text-center">
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isBookIncluded() {
|
||||
return !!this.collection.isBookIncluded
|
||||
},
|
||||
books() {
|
||||
return this.collection.books || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickNuxtLink() {
|
||||
this.$emit('close')
|
||||
},
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickAdd() {
|
||||
this.$emit('add', this.collection)
|
||||
},
|
||||
clickRem() {
|
||||
this.$emit('remove', this.collection)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
||||
<div class="w-20 max-w-20 text-center">
|
||||
<!-- <img src="/Logo.png" /> -->
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<!-- <template v-if="isEditing">
|
||||
<form @submit.prevent="submitUpdate">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow pr-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
||||
<div class="pl-2 flex items-center">
|
||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template> -->
|
||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||
</div>
|
||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons pt-px">remove</span></ui-btn>
|
||||
<!-- <span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
highlight: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false,
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isBookIncluded() {
|
||||
return !!this.collection.isBookIncluded
|
||||
},
|
||||
wrapperClass() {
|
||||
var classes = []
|
||||
if (this.highlight) classes.push('bg-bg bg-opacity-60')
|
||||
if (!this.isEditing) classes.push('cursor-pointer')
|
||||
return classes.join(' ')
|
||||
},
|
||||
books() {
|
||||
return this.collection.books || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickNuxtLink() {
|
||||
this.$emit('close')
|
||||
},
|
||||
mouseover() {
|
||||
if (this.isEditing) return
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickAdd() {
|
||||
this.$emit('add', this.collection)
|
||||
},
|
||||
clickRem() {
|
||||
this.$emit('remove', this.collection)
|
||||
},
|
||||
deleteClick() {
|
||||
if (this.isEditing) return
|
||||
this.$emit('delete', this.collection)
|
||||
},
|
||||
editClick() {
|
||||
this.isEditing = true
|
||||
this.isHovering = false
|
||||
},
|
||||
cancelEditing() {
|
||||
this.isEditing = false
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -7,7 +7,9 @@
|
||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||
<span class="material-icons">delete</span>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||
<span class="material-icons text-2xl">delete</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +18,7 @@
|
||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
||||
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
||||
><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input
|
||||
><span class="material-icons text-2xl inline-block md:!hidden">upload</span></ui-file-input
|
||||
>
|
||||
</div>
|
||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||
<p class="pl-4 text-base">
|
||||
Max episodes to keep
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||
<p class="pl-4 text-base">
|
||||
Max new episodes to download per check
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
<p class="text-lg">Make M4B Audiobook File</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.</p>
|
||||
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsMakeM4bDescription }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
@@ -23,12 +23,12 @@
|
||||
<div v-if="showMp3Split && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p class="text-lg">Split M4B to MP3's</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate multiple MP3's split by chapters with embedded metadata, cover image, and chapters.</p>
|
||||
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsSplitM4bDescription }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<ui-btn :disabled="true">Not yet implemented</ui-btn>
|
||||
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,8 +37,8 @@
|
||||
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p class="text-lg">Embed Metadata</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Embed metadata into audio files including cover image and chapters.</p>
|
||||
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsEmbedMetadataDescription }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<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>
|
||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||
<span class="material-icons text-success">play_arrow</span>
|
||||
<span class="material-icons text-2xl text-success">play_arrow</span>
|
||||
</button>
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||
<span class="material-icons text-error">close</span>
|
||||
<span class="material-icons text-2xl text-error">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||
|
||||
191
client/components/modals/playlists/AddCreateModal.vue
Normal file
191
client/components/modals/playlists/AddCreateModal.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div class="py-4 px-4">
|
||||
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||
<h1 v-else class="text-2xl">{{ $getString('LabelAddToPlaylistBatch', [selectedPlaylistItems.length]) }}</h1>
|
||||
</div>
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<transition-group name="list-complete" tag="div">
|
||||
<template v-for="playlist in sortedPlaylists">
|
||||
<modals-playlists-user-playlist-item :key="playlist.id" :playlist="playlist" class="list-complete-item" @add="addToPlaylist" @remove="removeFromPlaylist" @close="show = false" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||
<form @submit.prevent="submitCreatePlaylist">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="flex-grow px-2">
|
||||
<ui-text-input v-model="newPlaylistName" :placeholder="$strings.PlaceholderNewPlaylist" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
newPlaylistName: '',
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.loadPlaylists()
|
||||
this.newPlaylistName = ''
|
||||
} else {
|
||||
this.$store.commit('globals/setSelectedPlaylistItems', null)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.globals.showPlaylistsModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('globals/setShowPlaylistsModal', val)
|
||||
}
|
||||
},
|
||||
title() {
|
||||
if (!this.selectedPlaylistItems.length) return ''
|
||||
if (this.isBatch) {
|
||||
return this.$getString('MessageItemsSelected', [this.selectedPlaylistItems.length])
|
||||
}
|
||||
const selectedPlaylistItem = this.selectedPlaylistItems[0]
|
||||
if (selectedPlaylistItem.episode) {
|
||||
return selectedPlaylistItem.episode.title
|
||||
}
|
||||
return selectedPlaylistItem.libraryItem.media.metadata.title || ''
|
||||
},
|
||||
playlists() {
|
||||
return this.$store.state.libraries.userPlaylists || []
|
||||
},
|
||||
sortedPlaylists() {
|
||||
return this.playlists
|
||||
.map((playlist) => {
|
||||
const includesItem = !this.selectedPlaylistItems.some((item) => !this.checkIsItemInPlaylist(playlist, item))
|
||||
|
||||
return {
|
||||
isItemIncluded: includesItem,
|
||||
...playlist
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.isItemIncluded ? -1 : 1))
|
||||
},
|
||||
isBatch() {
|
||||
return this.selectedPlaylistItems.length > 1
|
||||
},
|
||||
selectedPlaylistItems() {
|
||||
return this.$store.state.globals.selectedPlaylistItems || []
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkIsItemInPlaylist(playlist, item) {
|
||||
if (item.episode) {
|
||||
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id && i.episodeId === item.episode.id)
|
||||
}
|
||||
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id)
|
||||
},
|
||||
loadPlaylists() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/playlists`)
|
||||
.then((data) => {
|
||||
this.$store.commit('libraries/setUserPlaylists', data.results || [])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get playlists', error)
|
||||
this.$toast.error('Failed to load user playlists')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
removeFromPlaylist(playlist) {
|
||||
if (!this.selectedPlaylistItems.length) return
|
||||
this.processing = true
|
||||
|
||||
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||
this.$axios
|
||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success('Playlist item(s) removed')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove items from playlist', error)
|
||||
this.$toast.error('Failed to remove playlist item(s)')
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
addToPlaylist(playlist) {
|
||||
if (!this.selectedPlaylistItems.length) return
|
||||
this.processing = true
|
||||
|
||||
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||
this.$axios
|
||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items added to playlist`, updatedPlaylist)
|
||||
this.$toast.success('Items added to playlist')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add items to playlist', error)
|
||||
this.$toast.error('Failed to add items to playlist')
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
submitCreatePlaylist() {
|
||||
if (!this.newPlaylistName || !this.selectedPlaylistItems.length) {
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
|
||||
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||
const newPlaylist = {
|
||||
items: itemObjects,
|
||||
libraryId: this.currentLibraryId,
|
||||
name: this.newPlaylistName
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$post('/api/playlists', newPlaylist)
|
||||
.then((data) => {
|
||||
console.log('New playlist created', data)
|
||||
this.$toast.success(`Playlist "${data.name}" created`)
|
||||
this.processing = false
|
||||
this.newPlaylistName = ''
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create playlist', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
125
client/components/modals/playlists/EditModal.vue
Normal file
125
client/components/modals/playlists/EditModal.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<covers-playlist-cover :items="items" :width="200" :height="200" />
|
||||
</div>
|
||||
<div class="flex-grow px-4">
|
||||
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" class="mb-2" />
|
||||
|
||||
<ui-textarea-with-label v-model="newPlaylistDescription" :label="$strings.LabelDescription" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
newPlaylistName: null,
|
||||
newPlaylistDescription: null,
|
||||
showImageUploader: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.globals.showEditPlaylistModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('globals/setShowEditPlaylistModal', val)
|
||||
}
|
||||
},
|
||||
playlist() {
|
||||
return this.$store.state.globals.selectedPlaylist || {}
|
||||
},
|
||||
playlistName() {
|
||||
return this.playlist.name
|
||||
},
|
||||
items() {
|
||||
return this.playlist.items || []
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.newPlaylistName = this.playlistName
|
||||
this.newPlaylistDescription = this.playlist.description || ''
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]))) {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||
.then(() => {
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) {
|
||||
return
|
||||
}
|
||||
if (!this.newPlaylistName) {
|
||||
return this.$toast.error('Playlist must have a name')
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
|
||||
var playlistUpdate = {
|
||||
name: this.newPlaylistName,
|
||||
description: this.newPlaylistDescription || null
|
||||
}
|
||||
this.$axios
|
||||
.$patch(`/api/playlists/${this.playlist.id}`, playlistUpdate)
|
||||
.then((playlist) => {
|
||||
console.log('Playlist Updated', playlist)
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
57
client/components/modals/playlists/UserPlaylistItem.vue
Normal file
57
client/components/modals/playlists/UserPlaylistItem.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
playlist: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isItemIncluded() {
|
||||
return !!this.playlist.isItemIncluded
|
||||
},
|
||||
items() {
|
||||
return this.playlist.items || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickNuxtLink() {
|
||||
this.$emit('close')
|
||||
},
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickAdd() {
|
||||
this.$emit('add', this.playlist)
|
||||
},
|
||||
clickRem() {
|
||||
this.$emit('remove', this.playlist)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||
|
||||
@@ -4,27 +4,37 @@
|
||||
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||
</ui-tooltip>
|
||||
|
||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl sm:text-2.5xl">snooze</span>
|
||||
<div v-else class="flex items-center">
|
||||
<span class="material-icons text-lg text-warning">snooze</span>
|
||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
|
||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
||||
<div v-else class="flex items-center">
|
||||
<span class="material-icons text-lg text-warning">snooze</span>
|
||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||
<span class="material-icons text-2xl sm:text-2.5xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||
</div>
|
||||
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
|
||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<div v-if="chapters.length" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span>
|
||||
</div>
|
||||
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||
<span class="material-icons text-2xl sm:text-3xl">queue_music</span>
|
||||
</button>
|
||||
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
||||
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
||||
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||
|
||||
@@ -92,13 +92,18 @@ export default {
|
||||
},
|
||||
ebookUrl() {
|
||||
if (!this.ebookFile) return null
|
||||
var itemRelPath = this.selectedLibraryItem.relPath
|
||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||
var relPath = this.ebookFile.metadata.relPath
|
||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||
let filepath = ''
|
||||
if (this.selectedLibraryItem.isFile) {
|
||||
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
|
||||
} 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}`)
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${relRelPath}`
|
||||
filepath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
||||
}
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${filepath}`
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<span class="material-icons text-7xl">show_chart</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsOverall }} {{ useOverallHours ? $strings.LabelStatsHours : $strings.LabelStatsDays }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
||||
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||
<span class="material-icons-outlined text-error">error_outline</span>
|
||||
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
||||
</ui-tooltip>
|
||||
|
||||
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
||||
@@ -64,13 +64,11 @@ export default {
|
||||
showConfirmApply: false,
|
||||
selectedBackup: null,
|
||||
isBackingUp: false,
|
||||
processing: false
|
||||
processing: false,
|
||||
backups: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backups() {
|
||||
return this.$store.state.backups || []
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
}
|
||||
@@ -96,9 +94,8 @@ export default {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$delete(`/api/backups/${backup.id}`)
|
||||
.then((backups) => {
|
||||
console.log('Backup deleted', backups)
|
||||
this.$store.commit('setBackups', backups)
|
||||
.then((data) => {
|
||||
this.setBackups(data.backups || [])
|
||||
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
@@ -117,10 +114,10 @@ export default {
|
||||
this.isBackingUp = true
|
||||
this.$axios
|
||||
.$post('/api/backups')
|
||||
.then((backups) => {
|
||||
.then((data) => {
|
||||
this.isBackingUp = false
|
||||
this.$toast.success(this.$strings.ToastBackupCreateSuccess)
|
||||
this.$store.commit('setBackups', backups)
|
||||
this.setBackups(data.backups || [])
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isBackingUp = false
|
||||
@@ -136,9 +133,8 @@ export default {
|
||||
|
||||
this.$axios
|
||||
.$post('/api/backups/upload', form)
|
||||
.then((result) => {
|
||||
console.log('Upload backup result', result)
|
||||
this.$store.commit('setBackups', result)
|
||||
.then((data) => {
|
||||
this.setBackups(data.backups || [])
|
||||
this.$toast.success(this.$strings.ToastBackupUploadSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
@@ -148,9 +144,29 @@ export default {
|
||||
this.$toast.error(errorMessage)
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
setBackups(backups) {
|
||||
backups.sort((a, b) => b.createdAt - a.createdAt)
|
||||
this.backups = backups
|
||||
},
|
||||
loadBackups() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get('/api/backups')
|
||||
.then((data) => {
|
||||
this.setBackups(data.backups || [])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load backups', error)
|
||||
this.$toast.error('Failed to load backups')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadBackups()
|
||||
if (this.$route.query.backup) {
|
||||
this.$toast.success('Backup applied successfully')
|
||||
this.$router.replace('/config')
|
||||
|
||||
119
client/components/tables/PlaylistItemsTable.vue
Normal file
119
client/components/tables/PlaylistItemsTable.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="w-full bg-primary bg-opacity-40">
|
||||
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
|
||||
<p class="pr-4">{{ $strings.HeaderPlaylistItems }}</p>
|
||||
|
||||
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
<draggable v-model="itemsCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||
<transition-group type="transition" :name="!drag ? 'playlist-item' : null">
|
||||
<template v-for="(item, index) in itemsCopy">
|
||||
<tables-playlist-item-table-row :key="index" :is-dragging="drag" :item="item" :playlist-id="playlistId" :book-cover-aspect-ratio="bookCoverAspectRatio" class="item" :class="drag ? '' : 'playlist-item-item'" @edit="editItem" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
props: {
|
||||
playlistId: String,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drag: false,
|
||||
dragOptions: {
|
||||
animation: 200,
|
||||
group: 'description',
|
||||
ghostClass: 'ghost'
|
||||
},
|
||||
itemsCopy: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
handler(newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
totalDuration() {
|
||||
var _total = 0
|
||||
this.items.forEach((item) => {
|
||||
if (item.episode) _total += item.episode.duration
|
||||
else _total += item.libraryItem.media.duration
|
||||
})
|
||||
return _total
|
||||
},
|
||||
totalDurationPretty() {
|
||||
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editItem(playlistItem) {
|
||||
if (playlistItem.episode) {
|
||||
this.$store.commit('globals/setSelectedEpisode', playlist.episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
} else {
|
||||
const itemIds = this.items.map((i) => i.libraryItemId)
|
||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||
this.$store.commit('showEditModal', playlistItem.libraryItem)
|
||||
}
|
||||
},
|
||||
draggableUpdate() {
|
||||
var playlistUpdate = {
|
||||
items: this.itemsCopy.map((i) => ({ libraryItemId: i.libraryItemId, episodeId: i.episodeId }))
|
||||
}
|
||||
this.$axios
|
||||
.$patch(`/api/playlists/${this.playlistId}`, playlistUpdate)
|
||||
.then((playlist) => {
|
||||
console.log('Playlist updated', playlist)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update playlist', error)
|
||||
this.$toast.error('Failed to save playlist items order')
|
||||
})
|
||||
},
|
||||
init() {
|
||||
this.itemsCopy = this.items.map((i) => ({ ...i }))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.playlist-item-item {
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.playlist-item-enter-from,
|
||||
.playlist-item-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.playlist-item-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderUsers }}</h1>
|
||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser">
|
||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<table id="accounts">
|
||||
<tr>
|
||||
@@ -26,11 +19,9 @@
|
||||
</td>
|
||||
<td class="text-sm">{{ user.type }}</td>
|
||||
<td class="hidden lg:table-cell">
|
||||
<div v-if="usersOnline[user.id] && usersOnline[user.id].session && usersOnline[user.id].session.libraryItem && usersOnline[user.id].session.libraryItem.media">
|
||||
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||
</div>
|
||||
<div v-else-if="user.mostRecent">
|
||||
<p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p>
|
||||
<div v-if="usersOnline[user.id]">
|
||||
<p v-if="usersOnline[user.id].session && usersOnline[user.id].session.libraryItem" class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||
<p v-else-if="usersOnline[user.id].mostRecent && usersOnline[user.id].mostRecent.media" class="truncate text-xs">Last: {{ usersOnline[user.id].mostRecent.media.metadata.title }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs font-mono hidden sm:table-cell">
|
||||
@@ -81,7 +72,7 @@ export default {
|
||||
},
|
||||
usersOnline() {
|
||||
var usermap = {}
|
||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
||||
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
||||
return usermap
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
<span class="material-icons text-2xl">play_arrow</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderLibraries }}</h1>
|
||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
|
||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="librariesTable">
|
||||
<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">
|
||||
<div :key="library.id" class="item">
|
||||
|
||||
237
client/components/tables/playlist/ItemTableRow.vue
Normal file
237
client/components/tables/playlist/ItemTableRow.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||
<div v-if="item" class="flex h-16 md:h-20">
|
||||
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||
<span class="material-icons text-2xl">play_arrow</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden max-w-48 md:max-w-md h-full flex items-center px-2 md:px-3">
|
||||
<div>
|
||||
<div class="truncate max-w-48 md:max-w-md">
|
||||
<nuxt-link :to="`/item/${libraryItem.id}`" class="truncate hover:underline text-sm md:text-base">{{ itemTitle }}</nuxt-link>
|
||||
</div>
|
||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||
<template v-for="(author, index) in bookAuthors">
|
||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||
</template>
|
||||
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-400">{{ itemDuration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
|
||||
<div class="flex h-full items-center">
|
||||
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||
</div>
|
||||
<div v-if="userCanDelete" class="mx-1">
|
||||
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
playlistId: String,
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isDragging: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isProcessingReadUpdate: false,
|
||||
processingRemove: false,
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isDragging: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.isHovering = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
translateDistance() {
|
||||
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
||||
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
||||
return '-translate-x-24'
|
||||
},
|
||||
libraryItem() {
|
||||
return this.item.libraryItem || {}
|
||||
},
|
||||
episode() {
|
||||
return this.item.episode
|
||||
},
|
||||
episodeId() {
|
||||
return this.episode ? this.episode.id : null
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
tracks() {
|
||||
if (this.episode) return []
|
||||
return this.media.tracks || []
|
||||
},
|
||||
itemTitle() {
|
||||
if (this.episode) return this.episode.title
|
||||
return this.mediaMetadata.title || ''
|
||||
},
|
||||
bookAuthors() {
|
||||
if (this.episode) return []
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
itemDuration() {
|
||||
if (this.episode) return this.$elapsedPretty(this.episode.duration)
|
||||
return this.$elapsedPretty(this.media.duration)
|
||||
},
|
||||
isMissing() {
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
isInvalid() {
|
||||
return this.libraryItem.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.$store.getters['getIsMediaStreaming'](this.libraryItem.id, this.episodeId)
|
||||
},
|
||||
showPlayBtn() {
|
||||
return !this.isMissing && !this.isInvalid && !this.isStreaming && (this.tracks.length || this.episode)
|
||||
},
|
||||
itemProgress() {
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
|
||||
},
|
||||
userIsFinished() {
|
||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||
},
|
||||
coverSize() {
|
||||
return this.$store.state.globals.isMobile ? 30 : 50
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||
return this.coverSize
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mouseover() {
|
||||
if (this.isDragging) return
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
},
|
||||
playClick() {
|
||||
let queueItem = null
|
||||
if (this.episode) {
|
||||
queueItem = {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
libraryId: this.libraryItem.libraryId,
|
||||
episodeId: this.episodeId,
|
||||
title: this.itemTitle,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: '',
|
||||
duration: this.media.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
}
|
||||
} else {
|
||||
queueItem = {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
libraryId: this.libraryItem.libraryId,
|
||||
episodeId: null,
|
||||
title: this.itemTitle,
|
||||
subtitle: this.bookAuthors.map((au) => au.name).join(', '),
|
||||
caption: '',
|
||||
duration: this.media.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
}
|
||||
}
|
||||
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
episodeId: this.episodeId,
|
||||
queueItems: [queueItem]
|
||||
})
|
||||
},
|
||||
clickEdit() {
|
||||
this.$emit('edit', this.item)
|
||||
},
|
||||
toggleFinished() {
|
||||
var updatePayload = {
|
||||
isFinished: !this.userIsFinished
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
|
||||
let routepath = `/api/me/progress/${this.libraryItem.id}`
|
||||
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||
|
||||
this.$axios
|
||||
.$patch(routepath, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
||||
})
|
||||
},
|
||||
removeClick() {
|
||||
this.processingRemove = true
|
||||
|
||||
let routepath = `/api/playlists/${this.playlistId}/item/${this.libraryItem.id}`
|
||||
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||
|
||||
this.$axios
|
||||
.$delete(routepath)
|
||||
.then((updatedPlaylist) => {
|
||||
if (!updatedPlaylist.items.length) {
|
||||
console.log(`All items removed so playlist was removed`, updatedPlaylist)
|
||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||
} else {
|
||||
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success('Item removed from playlist')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove item from playlist', error)
|
||||
this.$toast.error('Failed to remove item from playlist')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingRemove = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -16,18 +16,26 @@
|
||||
|
||||
<div class="flex items-center pt-2">
|
||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
||||
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
<span class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||
</button>
|
||||
|
||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||
</button>
|
||||
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
||||
</button> -->
|
||||
|
||||
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
|
||||
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
||||
</div>
|
||||
@@ -123,6 +131,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickAddToPlaylist() {
|
||||
this.$emit('addToPlaylist', this.episode)
|
||||
},
|
||||
clickedEpisode() {
|
||||
this.$emit('view', this.episode)
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||
<template v-for="episode in episodesSorted">
|
||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" />
|
||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
||||
</template>
|
||||
|
||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||
@@ -131,6 +131,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addToPlaylist(episode) {
|
||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||
},
|
||||
addEpisodeToQueue(episode) {
|
||||
const queueItem = {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
|
||||
<slot />
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
@@ -11,7 +10,6 @@
|
||||
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
||||
<slot />
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-icons">expand_more</span>
|
||||
<span class="material-icons text-2xl">expand_more</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="block truncate">{{ label }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-icons text-gray-100" aria-label="User Account" role="button">person</span>
|
||||
<span class="material-icons text-2xl text-gray-100" aria-label="User Account" role="button">person</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning mr-1" style="font-size: 1rem" @click.stop="editItem(item)">edit</span>
|
||||
<span v-if="showEdit" class="material-icons text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
||||
</div>
|
||||
{{ item[textKey] }}
|
||||
|
||||
@@ -21,7 +21,8 @@ export default {
|
||||
return {
|
||||
tooltip: null,
|
||||
tooltipId: null,
|
||||
isShowing: false
|
||||
isShowing: false,
|
||||
hideTimeout: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -46,10 +47,12 @@ export default {
|
||||
var tooltip = document.createElement('div')
|
||||
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
||||
tooltip.id = this.tooltipId
|
||||
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
||||
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
||||
tooltip.style.zIndex = 100
|
||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||
tooltip.innerHTML = this.text
|
||||
tooltip.addEventListener('mouseover', this.cancelHide);
|
||||
tooltip.addEventListener('mouseleave', this.hideTooltip);
|
||||
|
||||
this.setTooltipPosition(tooltip)
|
||||
|
||||
@@ -95,6 +98,7 @@ export default {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
this.isShowing = true
|
||||
},
|
||||
hideTooltip() {
|
||||
@@ -102,11 +106,16 @@ export default {
|
||||
this.tooltip.remove()
|
||||
this.isShowing = false
|
||||
},
|
||||
cancelHide() {
|
||||
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
||||
},
|
||||
mouseover() {
|
||||
if (!this.isShowing) this.showTooltip()
|
||||
},
|
||||
mouseleave() {
|
||||
if (this.isShowing) this.hideTooltip()
|
||||
if (this.isShowing) {
|
||||
this.hideTimeout = setTimeout(this.hideTooltip, 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -101,35 +101,35 @@ export default {
|
||||
intervalOptions() {
|
||||
return [
|
||||
{
|
||||
text: 'Custom daily/weekly',
|
||||
text: this.$strings.LabelIntervalCustomDailyWeekly,
|
||||
value: 'custom'
|
||||
},
|
||||
{
|
||||
text: 'Every day',
|
||||
text: this.$strings.LabelIntervalEveryDay,
|
||||
value: 'daily'
|
||||
},
|
||||
{
|
||||
text: 'Every 12 hours',
|
||||
text: this.$strings.LabelIntervalEvery12Hours,
|
||||
value: '0 */12 * * *'
|
||||
},
|
||||
{
|
||||
text: 'Every 6 hours',
|
||||
text: this.$strings.LabelIntervalEvery6Hours,
|
||||
value: '0 */6 * * *'
|
||||
},
|
||||
{
|
||||
text: 'Every 2 hours',
|
||||
text: this.$strings.LabelIntervalEvery2Hours,
|
||||
value: '0 */2 * * *'
|
||||
},
|
||||
{
|
||||
text: 'Every hour',
|
||||
text: this.$strings.LabelIntervalEveryHour,
|
||||
value: '0 * * * *'
|
||||
},
|
||||
{
|
||||
text: 'Every 30 minutes',
|
||||
text: this.$strings.LabelIntervalEvery30Minutes,
|
||||
value: '*/30 * * * *'
|
||||
},
|
||||
{
|
||||
text: 'Every 15 minutes',
|
||||
text: this.$strings.LabelIntervalEvery15Minutes,
|
||||
value: '*/15 * * * *'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -101,14 +101,14 @@ export default {
|
||||
this.updateSelectionMode(this.isSelectionMode)
|
||||
},
|
||||
updateSelectionMode(val) {
|
||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
||||
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||
|
||||
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
|
||||
component = component[0]
|
||||
component.setSelectionMode(val)
|
||||
component.selected = selectedLibraryItems.includes(ent.id)
|
||||
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||
})
|
||||
},
|
||||
scrolled() {
|
||||
|
||||
@@ -63,7 +63,7 @@ export default {
|
||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -82,14 +82,14 @@ export default {
|
||||
this.updateSelectionMode(this.isSelectionMode)
|
||||
},
|
||||
updateSelectionMode(val) {
|
||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
||||
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||
|
||||
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
|
||||
component = component[0]
|
||||
component.setSelectionMode(val)
|
||||
component.selected = selectedLibraryItems.includes(item.id)
|
||||
component.selected = selectedMediaItems.some((i) => i.id === item.id)
|
||||
})
|
||||
},
|
||||
scrolled() {
|
||||
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
|
||||
<modals-item-edit-modal />
|
||||
<modals-collections-add-create-modal />
|
||||
<modals-edit-collection-modal />
|
||||
<modals-collections-edit-modal />
|
||||
<modals-playlists-add-create-modal />
|
||||
<modals-playlists-edit-modal />
|
||||
<modals-podcast-edit-episode />
|
||||
<modals-podcast-view-episode />
|
||||
<modals-authors-edit-modal />
|
||||
@@ -40,9 +42,8 @@ export default {
|
||||
if (this.$store.state.showEditModal) {
|
||||
this.$store.commit('setShowEditModal', false)
|
||||
}
|
||||
if (this.$store.state.selectedLibraryItems) {
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
}
|
||||
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.updateBodyClass()
|
||||
}
|
||||
},
|
||||
@@ -53,9 +54,12 @@ export default {
|
||||
isCasting() {
|
||||
return this.$store.state.globals.isCasting
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
isShowingSideRail() {
|
||||
if (!this.$route.name) return false
|
||||
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
|
||||
return !this.$route.name.startsWith('config') && this.currentLibraryId
|
||||
},
|
||||
isShowingToolbar() {
|
||||
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
||||
@@ -87,19 +91,19 @@ export default {
|
||||
this.socket.emit('auth', token)
|
||||
|
||||
if (!this.isFirstSocketConnection || this.socketConnectionToastId !== null) {
|
||||
this.updateSocketConnectionToast('Socket Connected', 'success', 5000)
|
||||
this.updateSocketConnectionToast(this.$strings.ToastSocketConnected, 'success', 5000)
|
||||
}
|
||||
this.isFirstSocketConnection = false
|
||||
this.isSocketConnected = true
|
||||
},
|
||||
connectError() {
|
||||
console.error('[SOCKET] connect error')
|
||||
this.updateSocketConnectionToast('Socket Failed to Connect', 'error', null)
|
||||
this.updateSocketConnectionToast(this.$strings.ToastSocketFailedToConnect, 'error', null)
|
||||
},
|
||||
disconnect() {
|
||||
console.log('[SOCKET] Disconnected')
|
||||
this.isSocketConnected = false
|
||||
this.updateSocketConnectionToast('Socket Disconnected', 'error', null)
|
||||
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||
},
|
||||
reconnect() {
|
||||
console.error('[SOCKET] reconnected')
|
||||
@@ -132,14 +136,8 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
if (payload.backups && payload.backups.length) {
|
||||
this.$store.commit('setBackups', payload.backups)
|
||||
}
|
||||
if (payload.usersOnline) {
|
||||
this.$store.commit('users/resetUsers')
|
||||
payload.usersOnline.forEach((user) => {
|
||||
this.$store.commit('users/updateUser', user)
|
||||
})
|
||||
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit('socket_init')
|
||||
@@ -174,7 +172,7 @@ export default {
|
||||
this.$store.commit('libraries/remove', library)
|
||||
|
||||
// When removed currently selected library then set next accessible library
|
||||
const currLibraryId = this.$store.state.libraries.currentLibraryId
|
||||
const currLibraryId = this.currentLibraryId
|
||||
if (currLibraryId === library.id) {
|
||||
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
||||
if (nextLibrary) {
|
||||
@@ -213,7 +211,7 @@ export default {
|
||||
libraryItemRemoved(item) {
|
||||
if (this.$route.name.startsWith('item')) {
|
||||
if (this.$route.params.id === item.id) {
|
||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -286,31 +284,52 @@ export default {
|
||||
}
|
||||
},
|
||||
userOnline(user) {
|
||||
this.$store.commit('users/updateUser', user)
|
||||
this.$store.commit('users/updateUserOnline', user)
|
||||
},
|
||||
userOffline(user) {
|
||||
this.$store.commit('users/removeUser', user)
|
||||
this.$store.commit('users/removeUserOnline', user)
|
||||
},
|
||||
userStreamUpdate(user) {
|
||||
this.$store.commit('users/updateUser', user)
|
||||
this.$store.commit('users/updateUserOnline', user)
|
||||
},
|
||||
userMediaProgressUpdate(payload) {
|
||||
this.$store.commit('user/updateMediaProgress', payload)
|
||||
},
|
||||
collectionAdded(collection) {
|
||||
if (this.currentLibraryId !== collection.libraryId) return
|
||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||
},
|
||||
collectionUpdated(collection) {
|
||||
if (this.currentLibraryId !== collection.libraryId) return
|
||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||
},
|
||||
collectionRemoved(collection) {
|
||||
if (this.currentLibraryId !== collection.libraryId) return
|
||||
if (this.$route.name.startsWith('collection')) {
|
||||
if (this.$route.params.id === collection.id) {
|
||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/collections`)
|
||||
}
|
||||
}
|
||||
this.$store.commit('libraries/removeCollection', collection)
|
||||
},
|
||||
playlistAdded(playlist) {
|
||||
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||
},
|
||||
playlistUpdated(playlist) {
|
||||
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||
},
|
||||
playlistRemoved(playlist) {
|
||||
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||
|
||||
if (this.$route.name.startsWith('playlist')) {
|
||||
if (this.$route.params.id === playlist.id) {
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/playlists`)
|
||||
}
|
||||
}
|
||||
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
this.$store.commit('feeds/addFeed', data)
|
||||
},
|
||||
@@ -333,6 +352,9 @@ export default {
|
||||
this.$toast.info(toast)
|
||||
}
|
||||
},
|
||||
adminMessageEvt(message) {
|
||||
this.$toast.info(message)
|
||||
},
|
||||
initializeSocket() {
|
||||
this.socket = this.$nuxtSocket({
|
||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||
@@ -345,6 +367,7 @@ export default {
|
||||
this.$root.socket = this.socket
|
||||
console.log('Socket initialized')
|
||||
|
||||
// Pre-defined socket events
|
||||
this.socket.on('connect', this.connect)
|
||||
this.socket.on('connect_error', this.connectError)
|
||||
this.socket.on('disconnect', this.disconnect)
|
||||
@@ -353,6 +376,7 @@ export default {
|
||||
this.socket.io.on('reconnect_error', this.reconnectError)
|
||||
this.socket.io.on('reconnect_failed', this.reconnectFailed)
|
||||
|
||||
// Event received after authorizing socket
|
||||
this.socket.on('init', this.init)
|
||||
|
||||
// Stream Listeners
|
||||
@@ -382,11 +406,16 @@ export default {
|
||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
||||
|
||||
// User Collection Listeners
|
||||
// Collection Listeners
|
||||
this.socket.on('collection_added', this.collectionAdded)
|
||||
this.socket.on('collection_updated', this.collectionUpdated)
|
||||
this.socket.on('collection_removed', this.collectionRemoved)
|
||||
|
||||
// User Playlist Listeners
|
||||
this.socket.on('playlist_added', this.playlistAdded)
|
||||
this.socket.on('playlist_updated', this.playlistUpdated)
|
||||
this.socket.on('playlist_removed', this.playlistRemoved)
|
||||
|
||||
// Scan Listeners
|
||||
this.socket.on('scan_start', this.scanStart)
|
||||
this.socket.on('scan_complete', this.scanComplete)
|
||||
@@ -403,6 +432,8 @@ export default {
|
||||
this.socket.on('backup_applied', this.backupApplied)
|
||||
|
||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||
|
||||
this.socket.on('admin_message', this.adminMessageEvt)
|
||||
},
|
||||
showUpdateToast(versionData) {
|
||||
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
||||
@@ -472,9 +503,9 @@ export default {
|
||||
}
|
||||
|
||||
// Batch selecting
|
||||
if (this.$store.getters['getNumLibraryItemsSelected'] && name === 'Escape') {
|
||||
if (this.$store.getters['globals/getIsBatchSelectingMediaItems'] && name === 'Escape') {
|
||||
// ESCAPE key cancels batch selection
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
e.preventDefault()
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import LazyBookCard from '@/components/cards/LazyBookCard'
|
||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -15,6 +16,7 @@ export default {
|
||||
getComponentClass() {
|
||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||
return Vue.extend(LazyBookCard)
|
||||
},
|
||||
async mountEntityCard(index) {
|
||||
@@ -30,7 +32,7 @@ export default {
|
||||
shelfEl.appendChild(bookComponent.$el)
|
||||
if (this.isSelectionMode) {
|
||||
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
|
||||
} else {
|
||||
bookComponent.selected = false
|
||||
@@ -87,7 +89,7 @@ export default {
|
||||
}
|
||||
if (this.isSelectionMode) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,8 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
enabled: false,
|
||||
preCaching: [],
|
||||
runtimeCaching: []
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
5830
client/package-lock.json
generated
5830
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.4",
|
||||
"version": "2.2.7",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -85,6 +85,8 @@ export default {
|
||||
if (localStorage.getItem('token')) {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
this.$store.commit('libraries/setUserPlaylists', [])
|
||||
this.$store.commit('libraries/setCollections', [])
|
||||
this.$router.push('/login')
|
||||
},
|
||||
resetForm() {
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
<template>
|
||||
<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">
|
||||
<h1 class="text-xl">{{ title }}</h1>
|
||||
<h1 class="text-lg lg:text-xl">{{ title }}</h1>
|
||||
</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">
|
||||
<span class="material-icons text-base">edit</span>
|
||||
</button>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-base">{{ $strings.LabelDuration }}:</p>
|
||||
<p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
||||
<div class="flex-grow hidden md:block" />
|
||||
<p class="text-base hidden md:block">{{ $strings.LabelDuration }}:</p>
|
||||
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
||||
</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="flex items-center">
|
||||
<div class="w-12 hidden lg:block" />
|
||||
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
||||
<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" />
|
||||
<div class="w-40" />
|
||||
<div class="w-32 hidden lg:block" />
|
||||
</div>
|
||||
<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 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="w-40" />
|
||||
<div class="flex-grow" />
|
||||
<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 class="overflow-hidden">
|
||||
<transition name="slide">
|
||||
<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 items-center">
|
||||
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
|
||||
@@ -42,32 +45,34 @@
|
||||
</div>
|
||||
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
|
||||
</div>
|
||||
<div class="w-40"></div>
|
||||
<div class="w-32 hidden lg:block" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||
<div class="w-12"></div>
|
||||
<div class="w-32 px-2">{{ $strings.LabelStart }}</div>
|
||||
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></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="w-40"></div>
|
||||
<div class="w-32"></div>
|
||||
</div>
|
||||
<template v-for="chapter in newChapters">
|
||||
<div :key="chapter.id" class="flex py-1">
|
||||
<div class="w-12">#{{ chapter.id + 1 }}</div>
|
||||
<div class="w-32 px-1">
|
||||
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
|
||||
<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-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
||||
</div>
|
||||
<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 class="w-40 px-2 py-1">
|
||||
<div class="w-32 min-w-32 px-2 py-1">
|
||||
<div class="flex items-center">
|
||||
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
||||
<span class="material-icons-outlined text-base">remove</span>
|
||||
</button>
|
||||
<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)">
|
||||
<span class="material-icons-outlined text-base">remove</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
||||
@@ -75,11 +80,13 @@
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
||||
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
||||
</button>
|
||||
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
||||
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
||||
@@ -92,8 +99,15 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-xl py-4">
|
||||
<p class="text-lg mb-4 font-semibold py-1">{{ $strings.HeaderAudioTracks }}</p>
|
||||
<div class="w-full max-w-xl py-4 px-2">
|
||||
<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-grow">{{ $strings.LabelFilename }}</div>
|
||||
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
||||
@@ -173,8 +187,8 @@
|
||||
</div>
|
||||
<div class="flex items-center pt-2">
|
||||
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top">
|
||||
<span class="material-icons-outlined">info</span>
|
||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
||||
<span class="material-icons-outlined text-xl text-gray-200">info</span>
|
||||
</ui-tooltip>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
||||
@@ -186,6 +200,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
async asyncData({ store, params, app, redirect, from }) {
|
||||
if (!store.getters['user/getUserCanUpdate']) {
|
||||
@@ -228,7 +244,8 @@ export default {
|
||||
showFindChaptersModal: false,
|
||||
chapterData: null,
|
||||
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: {
|
||||
@@ -270,6 +287,23 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||
return
|
||||
@@ -300,7 +334,6 @@ export default {
|
||||
this.$store.commit('showEditModal', this.libraryItem)
|
||||
},
|
||||
addChapter(chapter) {
|
||||
console.log('Add chapter', chapter)
|
||||
const newChapter = {
|
||||
id: chapter.id + 1,
|
||||
start: chapter.start,
|
||||
@@ -315,22 +348,40 @@ export default {
|
||||
this.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++) {
|
||||
this.newChapters[i].id = i
|
||||
this.newChapters[i].start = Number(this.newChapters[i].start)
|
||||
|
||||
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) {
|
||||
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) {
|
||||
this.newChapters[i].error = 'Invalid start time must be < duration'
|
||||
this.newChapters[i].error = this.$strings.MessageChapterErrorStartGteDuration
|
||||
} else {
|
||||
this.newChapters[i].error = null
|
||||
}
|
||||
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) {
|
||||
console.log('Play Chapter', chapter.id)
|
||||
@@ -349,8 +400,6 @@ export default {
|
||||
const audioTrack = this.tracks.find((at) => {
|
||||
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
|
||||
})
|
||||
console.log('audio track', audioTrack)
|
||||
|
||||
this.selectedChapter = chapter
|
||||
this.isLoadingChapter = true
|
||||
|
||||
@@ -365,7 +414,6 @@ export default {
|
||||
if (this.$isDev) {
|
||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||
}
|
||||
console.log('src', src)
|
||||
|
||||
audioEl.src = src
|
||||
audioEl.id = 'chapter-audio'
|
||||
@@ -409,11 +457,11 @@ export default {
|
||||
|
||||
for (let i = 0; i < this.newChapters.length; i++) {
|
||||
if (this.newChapters[i].error) {
|
||||
this.$toast.error('Chapters have errors')
|
||||
this.$toast.error(this.$strings.ToastChaptersHaveErrors)
|
||||
return
|
||||
}
|
||||
if (!this.newChapters[i].title) {
|
||||
this.$toast.error('Chapters must have titles')
|
||||
this.$toast.error(this.$strings.ToastChaptersMustHaveTitles)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -509,22 +557,38 @@ export default {
|
||||
this.$toast.error('Failed to find chapters')
|
||||
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() {
|
||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||
this.asinInput = this.mediaMetadata.asin || null
|
||||
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
||||
if (!this.newChapters.length) {
|
||||
this.newChapters = [
|
||||
{
|
||||
id: 0,
|
||||
start: 0,
|
||||
end: this.mediaDuration,
|
||||
title: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
this.initChapters()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyAudioEl()
|
||||
|
||||
@@ -247,7 +247,7 @@ export default {
|
||||
cancelEncodeClick() {
|
||||
this.isCancelingEncode = true
|
||||
this.$axios
|
||||
.$post(`/api/encode-m4b/${this.libraryItemId}/cancel`)
|
||||
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||
.then(() => {
|
||||
this.$toast.success('Encode canceled')
|
||||
})
|
||||
@@ -262,7 +262,7 @@ export default {
|
||||
encodeM4bClick() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get(`/api/encode-m4b/${this.libraryItemId}`)
|
||||
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||
.then(() => {
|
||||
console.log('Ab m4b merge started')
|
||||
})
|
||||
@@ -287,7 +287,7 @@ export default {
|
||||
updateAudioFileMetadata() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$get(`/api/items/${this.libraryItemId}/audio-metadata?tone=1`)
|
||||
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?tone=1`)
|
||||
.then(() => {
|
||||
console.log('Audio metadata encode started')
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="border border-white border-opacity-10 max-w-7xl mx-auto mb-10 mt-5">
|
||||
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="material-icons">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
||||
<span class="material-icons text-2xl">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
||||
|
||||
<p class="ml-4 text-gray-200 text-lg">Map details</p>
|
||||
</div>
|
||||
@@ -91,11 +91,13 @@
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, redirect, app }) {
|
||||
if (!store.state.selectedLibraryItems.length) {
|
||||
if (!store.state.globals.selectedMediaItems.length) {
|
||||
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)
|
||||
const libraryItems = await app.$axios.$post(`/api/items/batch/get`, { libraryItemIds }).catch((error) => {
|
||||
const errorMsg = error.response.data || 'Failed to get items'
|
||||
console.error(errorMsg, error)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
|
||||
@@ -52,6 +52,11 @@ export default {
|
||||
return redirect('/')
|
||||
}
|
||||
|
||||
// If collection is a different library then set library as current
|
||||
if (collection.libraryId !== store.state.libraries.currentLibraryId) {
|
||||
await store.dispatch('libraries/fetch', collection.libraryId)
|
||||
}
|
||||
|
||||
store.commit('libraries/addUpdateCollection', collection)
|
||||
return {
|
||||
collectionId: collection.id
|
||||
@@ -59,8 +64,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processingRemove: false,
|
||||
collectionCopy: {}
|
||||
processingRemove: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -88,7 +92,7 @@ export default {
|
||||
})
|
||||
},
|
||||
streaming() {
|
||||
return !!this.playableBooks.find((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
||||
return !!this.playableBooks.some((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
||||
},
|
||||
showPlayButton() {
|
||||
return this.playableBooks.length
|
||||
@@ -105,19 +109,19 @@ export default {
|
||||
this.$store.commit('globals/setEditCollection', this.collection)
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) {
|
||||
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||
this.processingRemove = true
|
||||
var collectionName = this.collectionName
|
||||
this.$axios
|
||||
.$delete(`/api/collections/${this.collection.id}`)
|
||||
.then(() => {
|
||||
this.processingRemove = false
|
||||
this.$toast.success(`Collection "${collectionName}" Removed`)
|
||||
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove collection', error)
|
||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingRemove = false
|
||||
this.$toast.error(`Failed to remove collection`)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -165,4 +169,4 @@ export default {
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
||||
<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">
|
||||
<span class="material-icons cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
||||
<span class="material-icons text-2xl cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
||||
</div>
|
||||
<nuxt-child />
|
||||
@@ -42,10 +42,20 @@ export default {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
currentPage() {
|
||||
if (!this.$route.name) return 'Settings'
|
||||
if (!this.$route.name) return this.$strings.HeaderSettings
|
||||
var routeName = this.$route.name.split('-')
|
||||
if (routeName.length > 0) return routeName.slice(1).join('-')
|
||||
return 'Settings'
|
||||
if (routeName.length > 0) {
|
||||
const pageName = routeName.slice(1).join('-')
|
||||
if (pageName === 'log') return this.$strings.HeaderLogs
|
||||
else if (pageName === 'backups') return this.$strings.HeaderBackups
|
||||
else if (pageName === 'libraries') return this.$strings.HeaderLibraries
|
||||
else if (pageName === 'notifications') return this.$strings.HeaderNotifications
|
||||
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
||||
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||
}
|
||||
return this.$strings.HeaderSettings
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderBackups }}</h1>
|
||||
</div>
|
||||
|
||||
<p class="text-base mb-2 text-gray-300">{{ $strings.MessageBackupsDescription }} <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>.</p>
|
||||
<p class="text-base mb-4 text-gray-300">{{ $strings.MessageBackupsNote }}</p>
|
||||
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
||||
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
|
||||
@@ -17,7 +10,7 @@
|
||||
|
||||
<div v-if="enableBackups" class="mb-6">
|
||||
<div class="flex items-center pl-6">
|
||||
<span class="material-icons-outlined text-black-50">schedule</span>
|
||||
<span class="material-icons-outlined text-2xl text-black-50">schedule</span>
|
||||
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||
</div>
|
||||
@@ -40,9 +33,7 @@
|
||||
</div>
|
||||
|
||||
<tables-backups-table />
|
||||
</div>
|
||||
|
||||
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
|
||||
<div class="mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderSettings }}</h1>
|
||||
</div>
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderSettings">
|
||||
<div class="lg:flex">
|
||||
<div class="flex-1">
|
||||
<div class="pt-4">
|
||||
@@ -15,7 +11,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -25,7 +21,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -35,7 +31,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -57,7 +53,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -67,7 +63,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -93,7 +89,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsParseSubtitles }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -103,7 +99,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsFindCovers }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
<div class="flex-grow" />
|
||||
@@ -117,7 +113,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -127,7 +123,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -137,7 +133,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -147,7 +143,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -157,7 +153,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsDisableWatcher }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -172,7 +168,7 @@
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsExperimentalFeatures }}
|
||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</a>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
@@ -183,7 +179,7 @@
|
||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelSettingsEnableEReader }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -193,22 +189,23 @@
|
||||
<ui-tooltip text="Tone library for metadata">
|
||||
<p class="pl-4">
|
||||
Use Tone library for metadata
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||
|
||||
<div class="flex items-center py-4">
|
||||
<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="hidden lg:block mr-2" :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="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="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</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 class="flex items-center py-4">
|
||||
<div class="flex-grow" />
|
||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderLibraries" show-add-button @clicked="setShowLibraryModal">
|
||||
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
||||
</app-settings-content>
|
||||
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,66 +1,67 @@
|
||||
<template>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<h1 class="text-xl">{{ $strings.HeaderLibraryStats }}: {{ currentLibraryName }}</h1>
|
||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderLibraryStats + ': ' + currentLibraryName">
|
||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||
|
||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||
<template v-for="genre in top5Genres">
|
||||
<div :key="genre.genre" class="w-full py-2">
|
||||
<div class="flex items-end mb-1">
|
||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||
<div class="flex-grow" />
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
||||
{{ genre.genre }}
|
||||
</nuxt-link>
|
||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||
<template v-for="genre in top5Genres">
|
||||
<div :key="genre.genre" class="w-full py-2">
|
||||
<div class="flex items-end mb-1">
|
||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||
<div class="flex-grow" />
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
||||
{{ genre.genre }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||
<template v-for="(author, index) in top10Authors">
|
||||
<div :key="author.id" class="w-full py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||
</p>
|
||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
||||
</div>
|
||||
<div class="w-4 ml-3">
|
||||
<p class="text-sm font-bold">{{ author.count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||
<template v-for="(ab, index) in top10LongestItems">
|
||||
<div :key="index" class="w-full py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||
</p>
|
||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
||||
</div>
|
||||
<div class="w-4 ml-3">
|
||||
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||
<template v-for="(author, index) in top10Authors">
|
||||
<div :key="author.id" class="w-full py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||
</p>
|
||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
||||
</div>
|
||||
<div class="w-4 ml-3">
|
||||
<p class="text-sm font-bold">{{ author.count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||
<template v-for="(ab, index) in top10LongestItems">
|
||||
<div :key="index" class="w-full py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||
</p>
|
||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
||||
</div>
|
||||
<div class="w-4 ml-3">
|
||||
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderLogs }}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderLogs">
|
||||
<div class="flex justify-between mb-2 place-items-end">
|
||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||
|
||||
@@ -25,7 +22,7 @@
|
||||
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,20 +35,6 @@ export default {
|
||||
searchText: null,
|
||||
newServerSettings: {},
|
||||
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
|
||||
logLevels: [
|
||||
{
|
||||
value: 1,
|
||||
text: 'Debug'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: 'Info'
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: 'Warn'
|
||||
}
|
||||
],
|
||||
loadedLogs: []
|
||||
}
|
||||
},
|
||||
@@ -66,6 +49,22 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
logLevels() {
|
||||
return [
|
||||
{
|
||||
value: 1,
|
||||
text: this.$strings.LabelLogLevelDebug
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: this.$strings.LabelLogLevelInfo
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: this.$strings.LabelLogLevelWarn
|
||||
}
|
||||
]
|
||||
},
|
||||
logLevelItems() {
|
||||
if (process.env.NODE_ENV === 'production') return this.logLevels
|
||||
this.logLevels.unshift({ text: 'Trace', value: 0 })
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto">
|
||||
<h2 class="text-xl font-semibold mb-4">{{ $strings.HeaderAppriseNotificationSettings }}</h2>
|
||||
<p class="mb-6 text-gray-200">
|
||||
In order to use this feature you will need to have an instance of <a href="https://github.com/caronc/apprise-api" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
|
||||
<span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337</span> then you would put <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337/notify</span>.
|
||||
</p>
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderAppriseNotificationSettings" :description="$strings.MessageAppriseDescription">
|
||||
<form @submit.prevent="submitForm">
|
||||
<ui-text-input-with-label ref="apiUrlInput" v-model="appriseApiUrl" :disabled="savingSettings" label="Apprise API Url" class="mb-2" />
|
||||
|
||||
@@ -44,7 +38,7 @@
|
||||
<template v-for="notification in notifications">
|
||||
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
|
||||
</template>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" @update="updateSettings" />
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">{{ $strings.HeaderListeningSessions }}</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderListeningSessions">
|
||||
<div class="flex justify-end mb-2">
|
||||
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
|
||||
</div>
|
||||
@@ -56,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
||||
</div>
|
||||
@@ -88,6 +84,7 @@ export default {
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0,
|
||||
itemsPerPage: 10,
|
||||
userFilter: null,
|
||||
selectedUser: '',
|
||||
processingGoToTimestamp: false
|
||||
@@ -101,7 +98,7 @@ export default {
|
||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||
},
|
||||
userItems() {
|
||||
var userItems = [{ value: '', text: 'All Users' }]
|
||||
var userItems = [{ value: '', text: this.$strings.LabelAllUsers }]
|
||||
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||
},
|
||||
filteredUserUsername() {
|
||||
@@ -112,6 +109,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
removedSession() {
|
||||
// If on last page and this was the last session then load prev page
|
||||
if (this.currentPage == this.numPages - 1) {
|
||||
const newTotal = this.total - 1
|
||||
const newNumPages = Math.ceil(newTotal / this.itemsPerPage)
|
||||
if (newNumPages < this.numPages) {
|
||||
this.prevPage()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.loadSessions(this.currentPage)
|
||||
},
|
||||
async clickCurrentTime(session) {
|
||||
@@ -208,7 +215,7 @@ export default {
|
||||
},
|
||||
async loadSessions(page) {
|
||||
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
||||
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => {
|
||||
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -1,69 +1,68 @@
|
||||
<template>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<h1 class="text-xl">{{ $strings.HeaderYourStats }}</h1>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderYourStats">
|
||||
<div class="flex justify-center">
|
||||
<div class="flex p-2">
|
||||
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="px-3">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="flex p-2">
|
||||
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="px-3">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
||||
<div class="flex p-2">
|
||||
<div class="hidden sm:block">
|
||||
<span class="hidden sm:block material-icons-outlined text-5xl lg:text-6xl">event</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<div class="hidden sm:block">
|
||||
<span class="material-icons-outlined text-5xl lg:text-6xl">watch_later</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<div class="hidden sm:block">
|
||||
<span class="hidden sm:block material-icons-outlined text-5xl lg:text-6xl">event</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<div class="hidden sm:block">
|
||||
<span class="material-icons-outlined text-5xl lg:text-6xl">watch_later</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
|
||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<div class="flex mb-4 items-center">
|
||||
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
||||
</div>
|
||||
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||
<div :key="item.id" class="w-full py-0.5">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||
<div class="w-56">
|
||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="w-18 text-right">
|
||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
|
||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<div class="flex mb-4 items-center">
|
||||
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
||||
</div>
|
||||
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||
<div :key="item.id" class="w-full py-0.5">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||
<div class="w-56">
|
||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="w-18 text-right">
|
||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
||||
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export default {
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0,
|
||||
itemsPerPage: 10,
|
||||
processingGoToTimestamp: false
|
||||
}
|
||||
},
|
||||
@@ -99,6 +100,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
removedSession() {
|
||||
// If on last page and this was the last session then load prev page
|
||||
if (this.currentPage == this.numPages - 1) {
|
||||
const newTotal = this.total - 1
|
||||
const newNumPages = Math.ceil(newTotal / this.itemsPerPage)
|
||||
if (newNumPages < this.numPages) {
|
||||
this.prevPage()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.loadSessions(this.currentPage)
|
||||
},
|
||||
async clickCurrentTime(session) {
|
||||
@@ -191,7 +202,7 @@ export default {
|
||||
return 'Unknown'
|
||||
},
|
||||
async loadSessions(page) {
|
||||
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=10`).catch((err) => {
|
||||
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=${this.itemsPerPage}`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<tables-users-table />
|
||||
<app-settings-content :header-text="$strings.HeaderUsers" show-add-button @clicked="setShowUserModal">
|
||||
<tables-users-table />
|
||||
</app-settings-content>
|
||||
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
selectedAccount: null,
|
||||
showAccountModal: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
methods: {
|
||||
setShowUserModal(selectedAccount) {
|
||||
this.selectedAccount = selectedAccount
|
||||
this.showAccountModal = true
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<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-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||
<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>
|
||||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(narrator, index) in narrators">
|
||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(genre, index) in genres">
|
||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||
@@ -129,20 +129,20 @@
|
||||
<!-- Icon buttons -->
|
||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
||||
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
|
||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
|
||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_add'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" :bg-color="isQueued ? 'primary' : 'success bg-opacity-60'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
||||
<span class="material-icons text-2xl -ml-2 pr-2 text-white">auto_stories</span>
|
||||
{{ $strings.ButtonRead }}
|
||||
</ui-btn>
|
||||
|
||||
@@ -158,6 +158,10 @@
|
||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="!isPodcast && tracks.length" :text="$strings.LabelYourPlaylists" direction="top">
|
||||
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
@@ -608,6 +612,10 @@ export default {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||
},
|
||||
playlistsClick() {
|
||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem }])
|
||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||
},
|
||||
clickRSSFeed() {
|
||||
this.showRssFeedModal = true
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ params, query, store, app, redirect }) {
|
||||
async asyncData({ params, query, store, redirect }) {
|
||||
var libraryId = params.library
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
@@ -16,7 +16,6 @@ export default {
|
||||
|
||||
// Set series sort by
|
||||
if (params.id === 'series') {
|
||||
console.log('Series page', query)
|
||||
if (query.sort) {
|
||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
||||
|
||||
@@ -4,29 +4,41 @@
|
||||
|
||||
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||
<div class="w-full max-w-3xl mx-auto py-4">
|
||||
<p class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</p>
|
||||
<p class="text-xl mb-2 font-semibold px-4 md:px-0">{{ $strings.HeaderLatestEpisodes }}</p>
|
||||
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
|
||||
<template v-for="(episode, index) in episodesMapped">
|
||||
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
|
||||
<div class="flex-grow pl-4 max-w-2xl">
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||
<!-- mobile -->
|
||||
<div class="flex md:hidden mb-2">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||
<div class="flex-grow px-2">
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- desktop -->
|
||||
<div class="hidden md:block">
|
||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||
|
||||
<p class="font-semibold mb-2">{{ episode.title }}</p>
|
||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||
</div>
|
||||
|
||||
<p class="font-semibold mb-2 text-sm md:text-base">{{ episode.title }}</p>
|
||||
|
||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||
|
||||
<div class="flex items-center">
|
||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||
<span v-if="episodeIdStreaming === episode.id" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
<span v-else class="material-icons text-success">play_arrow</span>
|
||||
<span v-if="episodeIdStreaming === episode.id" class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||
<span v-else class="material-icons text-2xl text-success">play_arrow</span>
|
||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
||||
</button>
|
||||
|
||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
||||
<span class="material-icons-outlined">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
187
client/pages/playlist/_id.vue
Normal file
187
client/pages/playlist/_id.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 200px">
|
||||
<div class="relative" style="height: fit-content">
|
||||
<covers-playlist-cover :items="playlistItems" :width="200" :height="200" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
|
||||
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
|
||||
{{ playlistName }}
|
||||
</h1>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||
|
||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||
</div>
|
||||
|
||||
<div class="my-8 max-w-2xl">
|
||||
<p class="text-base text-gray-100">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlistId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, app, redirect, route }) {
|
||||
if (!store.state.user.user) {
|
||||
return redirect(`/login?redirect=${route.path}`)
|
||||
}
|
||||
var playlist = await app.$axios.$get(`/api/playlists/${params.id}`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
if (!playlist) {
|
||||
return redirect('/')
|
||||
}
|
||||
|
||||
// If playlist is a different library then set library as current
|
||||
if (playlist.libraryId !== store.state.libraries.currentLibraryId) {
|
||||
await store.dispatch('libraries/fetch', playlist.libraryId)
|
||||
}
|
||||
|
||||
store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||
return {
|
||||
playlistId: playlist.id
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processingRemove: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
playlistItems() {
|
||||
return this.playlist.items || []
|
||||
},
|
||||
playlistName() {
|
||||
return this.playlist.name || ''
|
||||
},
|
||||
description() {
|
||||
return this.playlist.description || ''
|
||||
},
|
||||
playlist() {
|
||||
return this.$store.getters['libraries/getPlaylist'](this.playlistId) || {}
|
||||
},
|
||||
playableItems() {
|
||||
return this.playlistItems.filter((item) => {
|
||||
const libraryItem = item.libraryItem
|
||||
if (libraryItem.isMissing || libraryItem.isInvalid) return false
|
||||
if (item.episode) return item.episode.audioFile
|
||||
return libraryItem.media.tracks.length
|
||||
})
|
||||
},
|
||||
streaming() {
|
||||
return !!this.playableItems.find((i) => this.$store.getters['getIsMediaStreaming'](i.libraryItemId, i.episodeId))
|
||||
},
|
||||
showPlayButton() {
|
||||
return this.playableItems.length
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editClick() {
|
||||
this.$store.commit('globals/setEditPlaylist', this.playlist)
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(`Are you sure you want to remove playlist "${this.playlistName}"?`)) {
|
||||
this.processingRemove = true
|
||||
var playlistName = this.playlistName
|
||||
this.$axios
|
||||
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||
.then(() => {
|
||||
this.processingRemove = false
|
||||
this.$toast.success(`Playlist "${playlistName}" Removed`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove playlist', error)
|
||||
this.processingRemove = false
|
||||
this.$toast.error(`Failed to remove playlist`)
|
||||
})
|
||||
}
|
||||
},
|
||||
clickPlay() {
|
||||
const queueItems = []
|
||||
|
||||
// Playlist queue will start at the first unfinished item
|
||||
// if all items are finished then entire playlist is queued
|
||||
const itemsWithProgress = this.playableItems.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
progress: this.$store.getters['user/getUserMediaProgress'](item.libraryItemId, item.episodeId)
|
||||
}
|
||||
})
|
||||
|
||||
const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished)
|
||||
if (!hasUnfinishedItems) {
|
||||
console.warn('All items in playlist are finished - starting at first item')
|
||||
}
|
||||
|
||||
for (let i = 0; i < itemsWithProgress.length; i++) {
|
||||
const playlistItem = itemsWithProgress[i]
|
||||
if (!hasUnfinishedItems || !playlistItem.progress || !playlistItem.progress.isFinished) {
|
||||
const libraryItem = playlistItem.libraryItem
|
||||
if (playlistItem.episode) {
|
||||
queueItems.push({
|
||||
libraryItemId: libraryItem.id,
|
||||
libraryId: libraryItem.libraryId,
|
||||
episodeId: playlistItem.episode.id,
|
||||
title: playlistItem.episode.title,
|
||||
subtitle: libraryItem.media.metadata.title,
|
||||
caption: '',
|
||||
duration: playlistItem.episode.duration || null,
|
||||
coverPath: libraryItem.media.coverPath || null
|
||||
})
|
||||
} else {
|
||||
queueItems.push({
|
||||
libraryItemId: libraryItem.id,
|
||||
libraryId: libraryItem.libraryId,
|
||||
episodeId: null,
|
||||
title: libraryItem.media.metadata.title,
|
||||
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
|
||||
caption: '',
|
||||
duration: libraryItem.media.duration || null,
|
||||
coverPath: libraryItem.media.coverPath || null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queueItems.length >= 0) {
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: queueItems[0].libraryItemId,
|
||||
episodeId: queueItems[0].episodeId,
|
||||
queueItems
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- Picker display -->
|
||||
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
||||
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
|
||||
<p class="text-center text-sm my-5">or</p>
|
||||
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<div class="flex">
|
||||
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
||||
|
||||
@@ -8,6 +8,7 @@ const languageCodeMap = {
|
||||
'de': 'Deutsch',
|
||||
'en-us': 'English',
|
||||
// 'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'hr': 'Hrvatski',
|
||||
'it': 'Italiano',
|
||||
'pl': 'Polski',
|
||||
|
||||
@@ -94,13 +94,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
||||
|
||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!navigator.clipboard) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(str).then(() => {
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
}, (err) => {
|
||||
console.error('Clipboard copy failed', str, err)
|
||||
resolve(false)
|
||||
})
|
||||
} else {
|
||||
const el = document.createElement('textarea')
|
||||
|
||||
@@ -4,16 +4,21 @@ export const state = () => ({
|
||||
showBatchCollectionModal: false,
|
||||
showCollectionsModal: false,
|
||||
showEditCollectionModal: false,
|
||||
showPlaylistsModal: false,
|
||||
showEditPlaylistModal: false,
|
||||
showEditPodcastEpisode: false,
|
||||
showViewPodcastEpisodeModal: false,
|
||||
showConfirmPrompt: false,
|
||||
confirmPromptOptions: null,
|
||||
showEditAuthorModal: false,
|
||||
selectedEpisode: null,
|
||||
selectedPlaylistItems: null,
|
||||
selectedPlaylist: null,
|
||||
selectedCollection: null,
|
||||
selectedAuthor: null,
|
||||
selectedMediaItems: [],
|
||||
isCasting: false, // Actively casting
|
||||
isChromecastInitialized: false, // Script loaded
|
||||
isChromecastInitialized: false, // Script loadeds
|
||||
showBatchQuickMatchModal: false,
|
||||
dateFormats: [
|
||||
{
|
||||
@@ -60,6 +65,9 @@ export const getters = {
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||
}
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||
},
|
||||
getIsBatchSelectingMediaItems: (state) => {
|
||||
return state.selectedMediaItems.length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +87,12 @@ export const mutations = {
|
||||
setShowEditCollectionModal(state, val) {
|
||||
state.showEditCollectionModal = val
|
||||
},
|
||||
setShowPlaylistsModal(state, val) {
|
||||
state.showPlaylistsModal = val
|
||||
},
|
||||
setShowEditPlaylistModal(state, val) {
|
||||
state.showEditPlaylistModal = val
|
||||
},
|
||||
setShowEditPodcastEpisodeModal(state, val) {
|
||||
state.showEditPodcastEpisode = val
|
||||
},
|
||||
@@ -96,9 +110,16 @@ export const mutations = {
|
||||
state.selectedCollection = collection
|
||||
state.showEditCollectionModal = true
|
||||
},
|
||||
setEditPlaylist(state, playlist) {
|
||||
state.selectedPlaylist = playlist
|
||||
state.showEditPlaylistModal = true
|
||||
},
|
||||
setSelectedEpisode(state, episode) {
|
||||
state.selectedEpisode = episode
|
||||
},
|
||||
setSelectedPlaylistItems(state, items) {
|
||||
state.selectedPlaylistItems = items
|
||||
},
|
||||
showEditAuthorModal(state, author) {
|
||||
state.selectedAuthor = author
|
||||
state.showEditAuthorModal = true
|
||||
@@ -117,5 +138,28 @@ export const mutations = {
|
||||
},
|
||||
setShowBatchQuickMatchModal(state, val) {
|
||||
state.showBatchQuickMatchModal = val
|
||||
},
|
||||
resetSelectedMediaItems(state) {
|
||||
// Vue.set(state, 'selectedMediaItems', [])
|
||||
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 {
|
||||
// const newSel = state.selectedMediaItems.concat([{...item}])
|
||||
// Vue.set(state, 'selectedMediaItems', newSel)
|
||||
state.selectedMediaItems.push(item)
|
||||
}
|
||||
},
|
||||
setMediaItemSelected(state, { item, selected }) {
|
||||
const index = state.selectedMediaItems.findIndex(i => i.id === item.id)
|
||||
if (index && !selected) {
|
||||
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
||||
} else if (selected && !index) {
|
||||
state.selectedMediaItems.splice(index, 1, item)
|
||||
// var newSel = state.selectedMediaItems.concat([libraryItemId])
|
||||
// Vue.set(state, 'selectedMediaItems', newSel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,9 @@ export const state = () => ({
|
||||
showEReader: false,
|
||||
selectedLibraryItem: null,
|
||||
developerMode: false,
|
||||
selectedLibraryItems: [],
|
||||
processingBatch: false,
|
||||
previousPath: '/',
|
||||
showExperimentalFeatures: false,
|
||||
backups: [],
|
||||
bookshelfBookIds: [],
|
||||
openModal: null,
|
||||
innerModalOpen: false,
|
||||
@@ -30,14 +28,10 @@ export const state = () => ({
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getIsLibraryItemSelected: state => libraryItemId => {
|
||||
return !!state.selectedLibraryItems.includes(libraryItemId)
|
||||
},
|
||||
getServerSetting: state => key => {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getNumLibraryItemsSelected: state => state.selectedLibraryItems.length,
|
||||
getLibraryItemIdStreaming: state => {
|
||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
||||
},
|
||||
@@ -218,26 +212,6 @@ export const mutations = {
|
||||
setSelectedLibraryItem(state, 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) {
|
||||
state.processingBatch = val
|
||||
},
|
||||
@@ -245,9 +219,6 @@ export const mutations = {
|
||||
state.showExperimentalFeatures = val
|
||||
localStorage.setItem('experimental', val ? 1 : 0)
|
||||
},
|
||||
setBackups(state, val) {
|
||||
state.backups = val.sort((a, b) => b.createdAt - a.createdAt)
|
||||
},
|
||||
setOpenModal(state, val) {
|
||||
state.openModal = val
|
||||
},
|
||||
|
||||
@@ -9,10 +9,12 @@ export const state = () => ({
|
||||
issues: 0,
|
||||
folderLastUpdate: 0,
|
||||
filterData: null,
|
||||
numUserPlaylists: 0,
|
||||
seriesSortBy: 'name',
|
||||
seriesSortDesc: false,
|
||||
seriesFilterBy: 'all',
|
||||
collections: []
|
||||
collections: [],
|
||||
userPlaylists: []
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@@ -59,6 +61,9 @@ export const getters = {
|
||||
},
|
||||
getCollection: state => id => {
|
||||
return state.collections.find(c => c.id === id)
|
||||
},
|
||||
getPlaylist: state => id => {
|
||||
return state.userPlaylists.find(p => p.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,20 +107,26 @@ export const actions = {
|
||||
return false
|
||||
}
|
||||
|
||||
const libraryChanging = state.currentLibraryId !== libraryId
|
||||
return this.$axios
|
||||
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
||||
.then((data) => {
|
||||
var library = data.library
|
||||
var filterData = data.filterdata
|
||||
var issues = data.issues || 0
|
||||
const library = data.library
|
||||
const filterData = data.filterdata
|
||||
const issues = data.issues || 0
|
||||
const numUserPlaylists = data.numUserPlaylists
|
||||
|
||||
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
||||
|
||||
commit('addUpdate', library)
|
||||
commit('setLibraryIssues', issues)
|
||||
commit('setLibraryFilterData', filterData)
|
||||
commit('setNumUserPlaylists', numUserPlaylists)
|
||||
commit('setCurrentLibrary', libraryId)
|
||||
commit('setCollections', [])
|
||||
if (libraryChanging) {
|
||||
commit('setCollections', [])
|
||||
commit('setUserPlaylists', [])
|
||||
}
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -219,6 +230,9 @@ export const mutations = {
|
||||
setLibraryFilterData(state, filterData) {
|
||||
state.filterData = filterData
|
||||
},
|
||||
setNumUserPlaylists(state, numUserPlaylists) {
|
||||
state.numUserPlaylists = numUserPlaylists
|
||||
},
|
||||
updateFilterDataWithItem(state, libraryItem) {
|
||||
if (!libraryItem || !state.filterData) return
|
||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||
@@ -320,5 +334,22 @@ export const mutations = {
|
||||
},
|
||||
removeCollection(state, collection) {
|
||||
state.collections = state.collections.filter(c => c.id !== collection.id)
|
||||
},
|
||||
setUserPlaylists(state, playlists) {
|
||||
state.userPlaylists = playlists
|
||||
state.numUserPlaylists = playlists.length
|
||||
},
|
||||
addUpdateUserPlaylist(state, playlist) {
|
||||
const index = state.userPlaylists.findIndex(p => p.id === playlist.id)
|
||||
if (index >= 0) {
|
||||
state.userPlaylists.splice(index, 1, playlist)
|
||||
} else {
|
||||
state.userPlaylists.push(playlist)
|
||||
state.numUserPlaylists++
|
||||
}
|
||||
},
|
||||
removeUserPlaylist(state, playlist) {
|
||||
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
|
||||
state.numUserPlaylists = state.userPlaylists.length
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const state = () => ({
|
||||
value: 'audible.uk'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.au',
|
||||
text: 'Audible.com.au',
|
||||
value: 'audible.au'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,6 +56,10 @@ export const getters = {
|
||||
if (!state.user) return false
|
||||
if (getters.getUserCanAccessAllLibraries) return true
|
||||
return getters.getLibrariesAccessible.includes(libraryId)
|
||||
},
|
||||
getIsSeriesRemovedFromContinueListening: (state) => (seriesId) => {
|
||||
if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false
|
||||
return state.user.seriesHideFromContinueListening.includes(seriesId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
export const state = () => ({
|
||||
users: []
|
||||
usersOnline: []
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getIsUserOnline: state => id => {
|
||||
return state.users.find(u => u.id === id)
|
||||
return state.usersOnline.find(u => u.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@ export const actions = {
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
resetUsers(state) {
|
||||
state.users = []
|
||||
setUsersOnline(state, usersOnline) {
|
||||
state.usersOnline = usersOnline
|
||||
},
|
||||
updateUser(state, user) {
|
||||
var index = state.users.findIndex(u => u.id === user.id)
|
||||
updateUserOnline(state, user) {
|
||||
var index = state.usersOnline.findIndex(u => u.id === user.id)
|
||||
if (index >= 0) {
|
||||
state.users.splice(index, 1, user)
|
||||
state.usersOnline.splice(index, 1, user)
|
||||
} else {
|
||||
state.users.push(user)
|
||||
state.usersOnline.push(user)
|
||||
}
|
||||
},
|
||||
removeUser(state, user) {
|
||||
state.users = state.users.filter(u => u.id !== user.id)
|
||||
removeUserOnline(state, user) {
|
||||
state.usersOnline = state.usersOnline.filter(u => u.id !== user.id)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||
"ButtonApply": "Anwenden",
|
||||
"ButtonApplyChapters": "Kapitel anwenden",
|
||||
"ButtonAuthors": "Autor",
|
||||
"ButtonAuthors": "Autoren",
|
||||
"ButtonBrowseForFolder": "Ordnersuche",
|
||||
"ButtonCancel": "Abbrechen",
|
||||
"ButtonCancelEncode": "Abbruch der Verschlüsselung",
|
||||
@@ -13,8 +13,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
|
||||
"ButtonChooseAFolder": "Wähle einen Ordner",
|
||||
"ButtonChooseFiles": "Wähle eine Datei",
|
||||
"ButtonClearFilter": "Filter löschen",
|
||||
"ButtonCloseFeed": "Feed schließen",
|
||||
"ButtonCollections": "Sammlungen",
|
||||
"ButtonConfigureScanner": "Scannereinstellungen",
|
||||
"ButtonCreate": "Ertsellen",
|
||||
"ButtonCreateBackup": "Sicherung erstellen",
|
||||
"ButtonDelete": "Löschen",
|
||||
@@ -26,24 +28,25 @@
|
||||
"ButtonHome": "Startseite",
|
||||
"ButtonIssues": "Probleme",
|
||||
"ButtonLatest": "Neuste",
|
||||
"ButtonLibrary": "Bibliothek",
|
||||
"ButtonLogout": "Abmelden",
|
||||
"ButtonLookup": "Nachschlagen",
|
||||
"ButtonLibrary": "Bibliothek",
|
||||
"ButtonManageTracks": "Tracks verwalten",
|
||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||
"ButtonMatchAllAuthors": "Abgleich aller Autoren",
|
||||
"ButtonMatchBooks": "Abgleich der Bücher",
|
||||
"ButtonMatchAllAuthors": "Online-Abgleich aller Autoren",
|
||||
"ButtonMatchBooks": "Online-Abgleich aller Hörbücher",
|
||||
"ButtonNevermind": "Vergiss es",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Feed öffnen",
|
||||
"ButtonOpenManager": "Manager öffnen",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlaying": "Playing",
|
||||
"ButtonPlay": "Abspielen",
|
||||
"ButtonPlaying": "Spielt",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Bereinige alle Zwischenspeicher",
|
||||
"ButtonPurgeItemsCache": "Bereinige den Hörbuch/Podcast-Zwischenspeicher",
|
||||
"ButtonPurgeMediaProgress": "Bereinige die Hörfortschritte",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||
"ButtonQuickMatch": "Schnellabgleich",
|
||||
"ButtonRead": "Lese",
|
||||
"ButtonRemove": "Löschen",
|
||||
@@ -57,18 +60,20 @@
|
||||
"ButtonSave": "Speichern",
|
||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScan": "Durchsuchen",
|
||||
"ButtonScanLibrary": "Bibliothek durchsuchen",
|
||||
"ButtonSearch": "Suchen",
|
||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||
"ButtonSeries": "Serie",
|
||||
"ButtonSeries": "Serien",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Arbeitszeiten",
|
||||
"ButtonShow": "Anzeigen",
|
||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
||||
"ButtonSubmit": "Absenden",
|
||||
"ButtonSubmit": "Ok",
|
||||
"ButtonUpload": "Hochladen",
|
||||
"ButtonUploadBackup": "Sicherung hochladen",
|
||||
"ButtonUploadCover": "Cover hochladen",
|
||||
"ButtonUploadCover": "Titelbild hochladen",
|
||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||
"ButtonViewAll": "Alles anzeigen",
|
||||
"ButtonYes": "Ja",
|
||||
@@ -83,7 +88,7 @@
|
||||
"HeaderChooseAFolder": "Wähle einen Ordner",
|
||||
"HeaderCollection": "Sammlungen",
|
||||
"HeaderCollectionItems": "Sammlungseinträge",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCover": "Titelbild",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderEpisodes": "Episoden",
|
||||
"HeaderFiles": "Dateien",
|
||||
@@ -93,23 +98,25 @@
|
||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
||||
"HeaderLibraries": "Bibliotheken",
|
||||
"HeaderLibraryFiles": "Bibliotheksdateien",
|
||||
"HeaderLibraryFiles": "Alle Dateien",
|
||||
"HeaderLibraryStats": "Bibliotheksstatistiken",
|
||||
"HeaderListeningSessions": "Hörsitzungen",
|
||||
"HeaderListeningSessions": "Ereignisse",
|
||||
"HeaderListeningStats": "Hörstatistiken",
|
||||
"HeaderLogin": "Anmeldung",
|
||||
"HeaderLogs": "Protokolle",
|
||||
"HeaderMatch": "Übereinstimmung",
|
||||
"HeaderMatch": "Online-Abgleich",
|
||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||
"HeaderNewAccount": "Neues Konto",
|
||||
"HeaderNewLibrary": "Neue Bibliothek",
|
||||
"HeaderNotifications": "Benachrichtigungen",
|
||||
"HeaderOtherFiles": "Sonstige Dateien",
|
||||
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
|
||||
"HeaderOtherFiles": "Sonstige Dateien",
|
||||
"HeaderPermissions": "Berechtigungen",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlayerQueue": "Spieler Warteschlange",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||
"HeaderPreviewCover": "Vorschau Cover",
|
||||
"HeaderPreviewCover": "Vorschau Titelbild",
|
||||
"HeaderRemoveEpisode": "Episode löschen",
|
||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||
@@ -126,7 +133,7 @@
|
||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
||||
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||
"HeaderStatsRecentSessions": "Neueste Sitzungen",
|
||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
||||
"HeaderTools": "Werkzeuge",
|
||||
@@ -135,16 +142,22 @@
|
||||
"HeaderUpdateDetails": "Details aktualisieren",
|
||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||
"HeaderUsers": "Benutzer",
|
||||
"HeaderYourStats": "Deine Statistiken",
|
||||
"HeaderYourStats": "Eigene Statistik",
|
||||
"LabelAccountType": "Kontoart",
|
||||
"LabelAccountTypeAdmin": "Admin",
|
||||
"LabelAccountTypeGuest": "Gast",
|
||||
"LabelAccountTypeUser": "Benutzer",
|
||||
"LabelActivity": "Aktivitäten",
|
||||
"LabelAddedAt": "Hinzugefügt am",
|
||||
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
||||
"LabelAddToCollectionBatch": "Füge {0} Bücher der Sammlung hinzu",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "Alle",
|
||||
"LabelAllUsers": "Alle Benutzer",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
||||
"LabelAuthors": "Autoren",
|
||||
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
|
||||
"LabelBackToUser": "Zurück zum Benutzer",
|
||||
@@ -158,6 +171,7 @@
|
||||
"LabelChangePassword": "Passwort ändern",
|
||||
"LabelChaptersFound": "gefundene Kapitel",
|
||||
"LabelChapterTitle": "Kapitelüberschrift",
|
||||
"LabelClosePlayer": "Player schließen",
|
||||
"LabelCollapseSeries": "Serien zusammenfassen",
|
||||
"LabelCollections": "Sammlungen",
|
||||
"LabelComplete": "Vollständig",
|
||||
@@ -187,15 +201,18 @@
|
||||
"LabelEpisode": "Episode",
|
||||
"LabelEpisodeTitle": "Episodentitel",
|
||||
"LabelEpisodeType": "Episodentyp",
|
||||
"LabelExplicit": "Ausdrücklich",
|
||||
"LabelExplicit": "Explizit <br />(Altersbeschränkung)",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "Datei",
|
||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
||||
"LabelFileModified": "Datei geändert",
|
||||
"LabelFilename": "Dateiname",
|
||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||
"LabelFindEpisodes": "Episoden suchen",
|
||||
"LabelFinished": "Beendet",
|
||||
"LabelFinished": "beendet",
|
||||
"LabelFolder": "Ordner",
|
||||
"LabelFolders": "Verzeichnisse",
|
||||
"LabelGenre": "Kategorie",
|
||||
"LabelGenres": "Kategorien",
|
||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||
"LabelHour": "Stunde",
|
||||
@@ -204,8 +221,16 @@
|
||||
"LabelIncomplete": "Unvollständig",
|
||||
"LabelInProgress": "In Bearbeitung",
|
||||
"LabelInterval": "Intervall",
|
||||
"LabelIntervalCustomDailyWeekly": "Benutzerdefiniert Täglich/Wöchentlich",
|
||||
"LabelIntervalEvery12Hours": "Alle 12 Stunden",
|
||||
"LabelIntervalEvery15Minutes": "Alle 15 Minuten",
|
||||
"LabelIntervalEvery2Hours": "Alle 2 Stunden",
|
||||
"LabelIntervalEvery30Minutes": "Alle 30 Minuten",
|
||||
"LabelIntervalEvery6Hours": "Alle 6 Stunden",
|
||||
"LabelIntervalEveryDay": "Jeden Tag",
|
||||
"LabelIntervalEveryHour": "Jede Stunde",
|
||||
"LabelInvalidParts": "Ungültige Teile",
|
||||
"LabelItem": "Element/Eintrag",
|
||||
"LabelItem": "Hörbuch/Podcast",
|
||||
"LabelLanguage": "Sprache",
|
||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||
"LabelLastSeen": "Zuletzt angesehen",
|
||||
@@ -218,8 +243,11 @@
|
||||
"LabelLibraryName": "Bibliotheksname",
|
||||
"LabelLimit": "Begrenzung",
|
||||
"LabelListenAgain": "Erneut anhören",
|
||||
"LabelLogLevelDebug": "Fehlersuche",
|
||||
"LabelLogLevelInfo": "Informationen",
|
||||
"LabelLogLevelWarn": "Warnungen",
|
||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
||||
"LabelMarkSeries": "Serien markieren",
|
||||
"LabelMarkSeries": "Serien markieren als",
|
||||
"LabelMediaPlayer": "Mediaplayer",
|
||||
"LabelMediaType": "Medientyp",
|
||||
"LabelMetadataProvider": "Metadatenanbieter",
|
||||
@@ -229,33 +257,38 @@
|
||||
"LabelMissingParts": "Fehlende Teile",
|
||||
"LabelMore": "Mehr",
|
||||
"LabelName": "Name",
|
||||
"LabelNarrator": "Erzähler",
|
||||
"LabelNarrators": "Erzähler",
|
||||
"LabelNew": "Neu",
|
||||
"LabelNewestAuthors": "Neuste Autoren",
|
||||
"LabelNewestEpisodes": "Neueste Episoden",
|
||||
"LabelNewPassword": "Neues Passwort",
|
||||
"LabelNotes": "Hinweise",
|
||||
"LabelNotFinished": "Nicht Beendet",
|
||||
"LabelNotificationEvent": "Benachrichtigungs Event",
|
||||
"LabelNotFinished": "nicht beendet",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
|
||||
"LabelNotificationBodyTemplate": "Textvorlage",
|
||||
"LabelNotificationTitleTemplate": "Titelvorlage",
|
||||
"LabelNotificationEvent": "Benachrichtigungs Event",
|
||||
"LabelNotificationsMaxFailedAttempts": "Maximale Fehlversuche",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Benachrichtigungen werden deaktiviert, wenn sie mehrmals nicht gesendet werden können.",
|
||||
"LabelNotificationsMaxQueueSize": "Maximale Größe der Warteschlange für die Benachrichtigungsereignisse",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Es wird nur 1 Ereignis pro Sekunde ausgelöst. Ereignisse werden ignoriert, wenn die Warteschlange die maximale Größe erreicht hat. Dies verhindert Benachrichtigungsspamming.",
|
||||
"LabelNotificationTitleTemplate": "Titelvorlage",
|
||||
"LabelNotStarted": "Nicht begonnen",
|
||||
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
||||
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
||||
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
||||
"LabelPassword": "Passwort",
|
||||
"LabelPath": "Pfad",
|
||||
"LabelPermissionsAccessAllLibraries": "Darf auf alle Bibliotheken zugreifen",
|
||||
"LabelPermissionsAccessAllTags": "Darf auf alle Schlagwörter zugreifen",
|
||||
"LabelPermissionsAccessExplicitContent": "Darf auf explizite Inhalte zugreifen",
|
||||
"LabelPermissionsDelete": "Darf löschen",
|
||||
"LabelPermissionsDownload": "Darf herunterladen",
|
||||
"LabelPermissionsUpdate": "Darf aktualisieren",
|
||||
"LabelPermissionsUpload": "Darf hochladen",
|
||||
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
||||
"LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter",
|
||||
"LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte",
|
||||
"LabelPermissionsDelete": "Löschen",
|
||||
"LabelPermissionsDownload": "Herunterladen",
|
||||
"LabelPermissionsUpdate": "Aktualisieren",
|
||||
"LabelPermissionsUpload": "Hochladen",
|
||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Abspielmethode",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
@@ -264,20 +297,23 @@
|
||||
"LabelProvider": "Anbieter",
|
||||
"LabelPubDate": "Veröffentlichungsdatum",
|
||||
"LabelPublisher": "Herausgeber",
|
||||
"LabelPublishYear": "Erscheinungsjahr",
|
||||
"LabelPublishYear": "Jahr",
|
||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||
"LabelRecentSeries": "Aktuelle Serien",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||
"LabelRemoveCover": "Lösche Titelbild",
|
||||
"LabelRSSFeedOpen": "RSS Feed Offen",
|
||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||
"LabelRSSFeedURL": "RSS Feed URL",
|
||||
"LabelSearchTerm": "Begriff suchen",
|
||||
"LabelSearchTitle": "Titel suchen",
|
||||
"LabelSearchTitle": "Titel",
|
||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||
"LabelSeason": "Staffel",
|
||||
"LabelSequence": "Reihenfolge",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeries": "Serien",
|
||||
"LabelSeriesName": "Serienname",
|
||||
"LabelSeriesProgress": "Serienfortschritt",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||
"LabelSettingsChromecastSupport": "Chromecast-unterstützung",
|
||||
"LabelSettingsDateFormat": "Datumsformat",
|
||||
@@ -289,7 +325,7 @@
|
||||
"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.",
|
||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Hörbuch kein eingebettetes Cover oder ein Coverbild im Ordner hat, versucht der Scanner, ein Cover zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Hörbuch kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||
@@ -302,23 +338,24 @@
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
||||
"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.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Abgleich alle Bücher die bereits eine ASIN haben",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Abgleich alle Bücher die bereits eine ISBN 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",
|
||||
"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.",
|
||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Titelbilder in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird nur eine Datei mit dem Namen \"cover\" gespeichert.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Metadaten in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird eine Datei mit der Endung \".abs\" gespeichert.",
|
||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||
"LabelShowAll": "Alles anzeigen",
|
||||
"LabelSize": "Größe",
|
||||
"LabelSleepTimer": "Einschlaf-Timer",
|
||||
"LabelStart": "Start",
|
||||
"LabelStarted": "Gestartet",
|
||||
"LabelStartedAt": "Gestartet am",
|
||||
"LabelStartTime": "Startzeit",
|
||||
"LabelStatsAudioTracks": "Audio Tracks",
|
||||
"LabelStatsAudioTracks": "Audiodateien",
|
||||
"LabelStatsAuthors": "Autoren",
|
||||
"LabelStatsBestDay": "Bester Tag",
|
||||
"LabelStatsDailyAverage": "Tagesdurchschnitt",
|
||||
@@ -330,10 +367,12 @@
|
||||
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
||||
"LabelStatsMinutes": "Minuten",
|
||||
"LabelStatsMinutesListening": "Gehörte Minuten",
|
||||
"LabelStatsOverall": "Insgesamt",
|
||||
"LabelStatsOverallDays": "Gesamte Tage",
|
||||
"LabelStatsOverallHours": "Gesamte Stunden",
|
||||
"LabelStatsWeekListening": "Gehörte Wochen",
|
||||
"LabelSubtitle": "Untertitel",
|
||||
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Schlagwörter",
|
||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||
"LabelTimeListened": "Gehörte Zeit",
|
||||
@@ -341,9 +380,19 @@
|
||||
"LabelTimeRemaining": "{0} verbleibend",
|
||||
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
||||
"LabelTitle": "Titel",
|
||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
||||
"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.",
|
||||
"LabelTotalDuration": "Gesamtdauer",
|
||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Typ",
|
||||
"LabelUnknown": "Unbekannt",
|
||||
"LabelUpdateCover": "Titelbild aktualisieren",
|
||||
@@ -359,35 +408,49 @@
|
||||
"LabelUsername": "Benutzername",
|
||||
"LabelValue": "Wert",
|
||||
"LabelVersion": "Version",
|
||||
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
||||
"LabelViewChapters": "Kapitel anzeigen",
|
||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
||||
"LabelYourBookmarks": "Ihre Lesezeichen",
|
||||
"LabelYourProgress": "Ihre Fortschritte",
|
||||
"MessageBackupsDescription": "In Sicherungen werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder gespeichert",
|
||||
"MessageBackupsNote": "Die Sicherungen enthalten keine Dateien welche in Ihren Bibliotheksordnern gespeichert sind.",
|
||||
"LabelYourBookmarks": "Lesezeichen",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Fortschritt",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"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.",
|
||||
"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",
|
||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
||||
"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": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
||||
"MessageCheckingCron": "Überprüfe cron...",
|
||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||
"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?",
|
||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen 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?",
|
||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||
"MessageFetching": "Abrufen...",
|
||||
"MessageForceReScanDescription": "scannt alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu gescannt.",
|
||||
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} Hörsitzungen im letzten Jahr",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||
"MessageLoading": "Laden...",
|
||||
"MessageLoadingFolders": "Lade Ordner...",
|
||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||
@@ -408,6 +471,7 @@
|
||||
"MessageNoEpisodes": "Keine Episoden",
|
||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||
"MessageNoGenres": "Keine Kategorien",
|
||||
"MessageNoIssues": "Keine Probleme",
|
||||
"MessageNoItems": "Keine Elemente/Einträge",
|
||||
"MessageNoItemsFound": "Keine Elemente/Einträge gefunden",
|
||||
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
||||
@@ -417,18 +481,28 @@
|
||||
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
||||
"MessageNoResults": "Keine Ergebnisse",
|
||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Abgleich verwendet werden kann",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "oder",
|
||||
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||
"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.",
|
||||
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Übereinstimmungen, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
||||
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
||||
"MessageRemoveChapter": "Kapitel löschen",
|
||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
|
||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||
"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.",
|
||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
|
||||
"MessageThinking": "Nachdenken...",
|
||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||
@@ -436,6 +510,7 @@
|
||||
"MessageUploading": "Hochladen...",
|
||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
||||
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
||||
@@ -449,6 +524,7 @@
|
||||
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Suche...",
|
||||
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
||||
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
||||
@@ -473,6 +549,8 @@
|
||||
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
||||
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||
@@ -496,14 +574,21 @@
|
||||
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
||||
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
||||
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Element/Eintrag aus der Sammlung entfernt",
|
||||
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Element/Eintrag aus der Sammlung entfernt",
|
||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
||||
"WeekdayFriday": "Freitag",
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Check & Download New Episodes",
|
||||
"ButtonChooseAFolder": "Choose a folder",
|
||||
"ButtonChooseFiles": "Choose files",
|
||||
"ButtonClearFilter": "Clear Filter",
|
||||
"ButtonCloseFeed": "Close Feed",
|
||||
"ButtonCollections": "Collections",
|
||||
"ButtonConfigureScanner": "Configure Scanner",
|
||||
"ButtonCreate": "Create",
|
||||
"ButtonCreateBackup": "Create Backup",
|
||||
"ButtonDelete": "Delete",
|
||||
@@ -26,9 +28,9 @@
|
||||
"ButtonHome": "Home",
|
||||
"ButtonIssues": "Issues",
|
||||
"ButtonLatest": "Latest",
|
||||
"ButtonLibrary": "Library",
|
||||
"ButtonLogout": "Logout",
|
||||
"ButtonLookup": "Lookup",
|
||||
"ButtonLibrary": "Library",
|
||||
"ButtonManageTracks": "Manage Tracks",
|
||||
"ButtonMapChapterTitles": "Map Chapter Titles",
|
||||
"ButtonMatchAllAuthors": "Match All Authors",
|
||||
@@ -39,6 +41,7 @@
|
||||
"ButtonOpenManager": "Open Manager",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlaying": "Playing",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Purge All Cache",
|
||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||
@@ -58,9 +61,11 @@
|
||||
"ButtonSaveAndClose": "Save & Close",
|
||||
"ButtonSaveTracklist": "Save Tracklist",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScanLibrary": "Scan Library",
|
||||
"ButtonSearch": "Search",
|
||||
"ButtonSelectFolderPath": "Select Folder Path",
|
||||
"ButtonSeries": "Series",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Shift Times",
|
||||
"ButtonShow": "Show",
|
||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||
@@ -104,10 +109,12 @@
|
||||
"HeaderNewAccount": "New Account",
|
||||
"HeaderNewLibrary": "New Library",
|
||||
"HeaderNotifications": "Notifications",
|
||||
"HeaderOtherFiles": "Other Files",
|
||||
"HeaderOpenRSSFeed": "Open RSS Feed",
|
||||
"HeaderOtherFiles": "Other Files",
|
||||
"HeaderPermissions": "Permissions",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasts to Add",
|
||||
"HeaderPreviewCover": "Preview Cover",
|
||||
"HeaderRemoveEpisode": "Remove Episode",
|
||||
@@ -141,10 +148,16 @@
|
||||
"LabelAccountTypeGuest": "Guest",
|
||||
"LabelAccountTypeUser": "User",
|
||||
"LabelActivity": "Activity",
|
||||
"LabelAddedAt": "Added At",
|
||||
"LabelAddToCollection": "Add to Collection",
|
||||
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "All",
|
||||
"LabelAllUsers": "All Users",
|
||||
"LabelAuthor": "Author",
|
||||
"LabelAuthorFirstLast": "Author (First Last)",
|
||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||
"LabelAuthors": "Authors",
|
||||
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
|
||||
"LabelBackToUser": "Back to User",
|
||||
@@ -158,6 +171,7 @@
|
||||
"LabelChangePassword": "Change Password",
|
||||
"LabelChaptersFound": "chapters found",
|
||||
"LabelChapterTitle": "Chapter Title",
|
||||
"LabelClosePlayer": "Close player",
|
||||
"LabelCollapseSeries": "Collapse Series",
|
||||
"LabelCollections": "Collections",
|
||||
"LabelComplete": "Complete",
|
||||
@@ -190,12 +204,15 @@
|
||||
"LabelExplicit": "Explicit",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "File",
|
||||
"LabelFileBirthtime": "File Birthtime",
|
||||
"LabelFileModified": "File Modified",
|
||||
"LabelFilename": "Filename",
|
||||
"LabelFilterByUser": "Filter by User",
|
||||
"LabelFindEpisodes": "Find Episodes",
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folders",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
"LabelHour": "Hour",
|
||||
@@ -204,6 +221,14 @@
|
||||
"LabelIncomplete": "Incomplete",
|
||||
"LabelInProgress": "In Progress",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
|
||||
"LabelIntervalEvery12Hours": "Every 12 hours",
|
||||
"LabelIntervalEvery15Minutes": "Every 15 minutes",
|
||||
"LabelIntervalEvery2Hours": "Every 2 hours",
|
||||
"LabelIntervalEvery30Minutes": "Every 30 minutes",
|
||||
"LabelIntervalEvery6Hours": "Every 6 hours",
|
||||
"LabelIntervalEveryDay": "Every day",
|
||||
"LabelIntervalEveryHour": "Every hour",
|
||||
"LabelInvalidParts": "Invalid Parts",
|
||||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Language",
|
||||
@@ -218,6 +243,9 @@
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelListenAgain": "Listen Again",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Warn",
|
||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||
"LabelMarkSeries": "Mark Series",
|
||||
"LabelMediaPlayer": "Media Player",
|
||||
@@ -229,6 +257,7 @@
|
||||
"LabelMissingParts": "Missing Parts",
|
||||
"LabelMore": "More",
|
||||
"LabelName": "Name",
|
||||
"LabelNarrator": "Narrator",
|
||||
"LabelNarrators": "Narrators",
|
||||
"LabelNew": "New",
|
||||
"LabelNewestAuthors": "Newest Authors",
|
||||
@@ -236,15 +265,18 @@
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
"LabelNotificationEvent": "Notification Event",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
"LabelNotificationAvailableVariables": "Available variables",
|
||||
"LabelNotificationBodyTemplate": "Body Template",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotificationEvent": "Notification Event",
|
||||
"LabelNotificationsMaxFailedAttempts": "Max failed attempts",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
|
||||
"LabelNotificationsMaxQueueSize": "Max queue size for notification events",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotStarted": "Not Started",
|
||||
"LabelNumberOfBooks": "Number of Books",
|
||||
"LabelNumberOfEpisodes": "# of Episodes",
|
||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||
"LabelPassword": "Password",
|
||||
"LabelPath": "Path",
|
||||
@@ -256,6 +288,7 @@
|
||||
"LabelPermissionsUpdate": "Can Update",
|
||||
"LabelPermissionsUpload": "Can Upload",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
@@ -269,6 +302,8 @@
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||
"LabelRSSFeedURL": "RSS Feed URL",
|
||||
"LabelSearchTerm": "Search Term",
|
||||
@@ -278,6 +313,7 @@
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||
"LabelSettingsDateFormat": "Date Format",
|
||||
@@ -306,14 +342,15 @@
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||
"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",
|
||||
"LabelSettingsStoreMetadataWithItem": "Store metadata with item",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||
"LabelShowAll": "Show All",
|
||||
"LabelSize": "Size",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelStart": "Start",
|
||||
"LabelStarted": "Started",
|
||||
"LabelStartedAt": "Started At",
|
||||
@@ -330,10 +367,12 @@
|
||||
"LabelStatsItemsInLibrary": "Items in Library",
|
||||
"LabelStatsMinutes": "minutes",
|
||||
"LabelStatsMinutesListening": "Minutes Listening",
|
||||
"LabelStatsOverall": "Overall",
|
||||
"LabelStatsOverallDays": "Overall Days",
|
||||
"LabelStatsOverallHours": "Overall Hours",
|
||||
"LabelStatsWeekListening": "Week Listening",
|
||||
"LabelSubtitle": "Subtitle",
|
||||
"LabelSupportedFileTypes": "Supported File Types",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
@@ -341,9 +380,19 @@
|
||||
"LabelTimeRemaining": "{0} remaining",
|
||||
"LabelTimeToShift": "Time to shift in seconds",
|
||||
"LabelTitle": "Title",
|
||||
"LabelToolsEmbedMetadata": "Embed Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
||||
"LabelTotalDuration": "Total Duration",
|
||||
"LabelTotalTimeListened": "Total Time Listened",
|
||||
"LabelTrackFromFilename": "Track from Filename",
|
||||
"LabelTrackFromMetadata": "Track from Metadata",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Type",
|
||||
"LabelUnknown": "Unknown",
|
||||
"LabelUpdateCover": "Update Cover",
|
||||
@@ -359,23 +408,37 @@
|
||||
"LabelUsername": "Username",
|
||||
"LabelValue": "Value",
|
||||
"LabelVersion": "Version",
|
||||
"LabelViewBookmarks": "View bookmarks",
|
||||
"LabelViewChapters": "View chapters",
|
||||
"LabelViewQueue": "View player queue",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Weekdays to run",
|
||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||
"LabelYourBookmarks": "Your Bookmarks",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Your Progress",
|
||||
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in",
|
||||
"MessageBackupsNote": "Backups do not include any files stored in your library folders.",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"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",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Downloading episode",
|
||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||
"MessageEmbedFinished": "Embed Finished!",
|
||||
@@ -408,6 +471,7 @@
|
||||
"MessageNoEpisodes": "No Episodes",
|
||||
"MessageNoFoldersAvailable": "No Folders Available",
|
||||
"MessageNoGenres": "No Genres",
|
||||
"MessageNoIssues": "No Issues",
|
||||
"MessageNoItems": "No Items",
|
||||
"MessageNoItemsFound": "No items found",
|
||||
"MessageNoListeningSessions": "No Listening Sessions",
|
||||
@@ -417,18 +481,28 @@
|
||||
"MessageNoPodcastsFound": "No podcasts found",
|
||||
"MessageNoResults": "No Results",
|
||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Not yet implemented",
|
||||
"MessageNoUpdateNecessary": "No update necessary",
|
||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "or",
|
||||
"MessagePauseChapter": "Pause chapter playback",
|
||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||
"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.",
|
||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
||||
"MessageRemoveChapter": "Remove chapter",
|
||||
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
|
||||
"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",
|
||||
"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",
|
||||
"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}?",
|
||||
"MessageThinking": "Thinking...",
|
||||
"MessageUploaderItemFailed": "Failed to upload",
|
||||
@@ -436,6 +510,7 @@
|
||||
"MessageUploading": "Uploading...",
|
||||
"MessageValidCronExpression": "Valid cron expression",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
|
||||
"MessageXLibraryIsEmpty": "{0} Library is empty!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found",
|
||||
"MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found",
|
||||
"NoteChangeRootPassword": "Root user is the only user that can have an empty password",
|
||||
@@ -449,6 +524,7 @@
|
||||
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
|
||||
"PlaceholderNewCollection": "New collection name",
|
||||
"PlaceholderNewFolderPath": "New folder path",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Search..",
|
||||
"ToastAccountUpdateFailed": "Failed to update account",
|
||||
"ToastAccountUpdateSuccess": "Account updated",
|
||||
@@ -473,6 +549,8 @@
|
||||
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
||||
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
|
||||
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
|
||||
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
||||
"ToastCollectionRemoveFailed": "Failed to remove collection",
|
||||
@@ -496,14 +574,21 @@
|
||||
"ToastLibraryScanStarted": "Library scan started",
|
||||
"ToastLibraryUpdateFailed": "Failed to update library",
|
||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Failed to create podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast created successfully",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||
"ToastSessionDeleteSuccess": "Session deleted",
|
||||
"ToastSocketConnected": "Socket connected",
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Failed to delete user",
|
||||
"ToastUserDeleteSuccess": "User deleted",
|
||||
"WeekdayFriday": "Friday",
|
||||
|
||||
@@ -1,54 +1,601 @@
|
||||
{
|
||||
"ButtonAdd": "Add",
|
||||
"ButtonAddChapters": "Add Chapters",
|
||||
"ButtonAddPodcasts": "Add Podcasts",
|
||||
"ButtonAddYourFirstLibrary": "Add your first library",
|
||||
"ButtonApply": "Apply",
|
||||
"ButtonApplyChapters": "Apply Chapters",
|
||||
"ButtonAuthors": "Authors",
|
||||
"ButtonBrowseForFolder": "Browse for Folder",
|
||||
"ButtonCancel": "Cancel",
|
||||
"ButtonCancelEncode": "Cancel Encode",
|
||||
"ButtonChangeRootPassword": "Change Root Password",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Check & Download New Episodes",
|
||||
"ButtonChooseAFolder": "Choose a folder",
|
||||
"ButtonChooseFiles": "Choose files",
|
||||
"ButtonClearFilter": "Clear Filter",
|
||||
"ButtonCloseFeed": "Close Feed",
|
||||
"ButtonCollections": "Collections",
|
||||
"ButtonConfigureScanner": "Configure Scanner",
|
||||
"ButtonCreate": "Create",
|
||||
"ButtonCreateBackup": "Create Backup",
|
||||
"ButtonDelete": "Delete",
|
||||
"ButtonEditChapters": "Edit Chapters",
|
||||
"ButtonEditPodcast": "Edit Podcast",
|
||||
"ButtonForceReScan": "Force Re-Scan",
|
||||
"ButtonFullPath": "Full Path",
|
||||
"ButtonHide": "Hide",
|
||||
"ButtonHome": "Home",
|
||||
"ButtonIssues": "Issues",
|
||||
"ButtonLatest": "Latest",
|
||||
"ButtonLibrary": "Library",
|
||||
"ButtonSeries": "Series",
|
||||
"ButtonCollections": "Collections",
|
||||
"ButtonAuthors": "Authors",
|
||||
"ButtonSearch": "Search",
|
||||
"ButtonIssues": "Issues",
|
||||
"ButtonChangePasswordSubmit": "Submit",
|
||||
"ButtonLogout": "Logout",
|
||||
"ButtonLookup": "Lookup",
|
||||
"ButtonManageTracks": "Manage Tracks",
|
||||
"ButtonMapChapterTitles": "Map Chapter Titles",
|
||||
"ButtonMatchAllAuthors": "Match All Authors",
|
||||
"ButtonMatchBooks": "Match Books",
|
||||
"ButtonNevermind": "Nevermind",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Open Feed",
|
||||
"ButtonOpenManager": "Open Manager",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlaying": "Playing",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Purge All Cache",
|
||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQuickMatch": "Quick Match",
|
||||
"ButtonRead": "Read",
|
||||
"ButtonRemove": "Remove",
|
||||
"ButtonRemoveAll": "Remove All",
|
||||
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
||||
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
|
||||
"ButtonReScan": "Re-Scan",
|
||||
"ButtonReset": "Reset",
|
||||
"ButtonRestore": "Restore",
|
||||
"ButtonSave": "Save",
|
||||
"ButtonSaveAndClose": "Save & Close",
|
||||
"ButtonSaveTracklist": "Save Tracklist",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScanLibrary": "Scan Library",
|
||||
"ButtonSearch": "Search",
|
||||
"ButtonSelectFolderPath": "Select Folder Path",
|
||||
"ButtonSeries": "Series",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Shift Times",
|
||||
"ButtonShow": "Show",
|
||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||
"ButtonStartMetadataEmbed": "Start Metadata Embed",
|
||||
"ButtonSubmit": "Submit",
|
||||
"ButtonUpload": "Upload",
|
||||
"ButtonUploadBackup": "Upload Backup",
|
||||
"ButtonUploadCover": "Upload Cover",
|
||||
"ButtonUploadOPMLFile": "Upload OPML File",
|
||||
"ButtonViewAll": "View All",
|
||||
"ButtonYes": "Yes",
|
||||
"HeaderAccount": "Account",
|
||||
"HeaderChangePassword": "Change Password",
|
||||
"HeaderSettings": "Settings",
|
||||
"HeaderLibraries": "Libraries",
|
||||
"HeaderUsers": "Users",
|
||||
"HeaderListeningSessions": "Listening Sessions",
|
||||
"HeaderAdvanced": "Advanced",
|
||||
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
|
||||
"HeaderAudiobookTools": "Audiobook File Management Tools",
|
||||
"HeaderAudioTracks": "Audio Tracks",
|
||||
"HeaderBackups": "Backups",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderNotifications": "Notifications",
|
||||
"HeaderChangePassword": "Change Password",
|
||||
"HeaderChapters": "Chapters",
|
||||
"HeaderChooseAFolder": "Choose a Folder",
|
||||
"HeaderCollection": "Collection",
|
||||
"HeaderCollectionItems": "Collection Items",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderEpisodes": "Episodes",
|
||||
"HeaderFiles": "Files",
|
||||
"HeaderFindChapters": "Find Chapters",
|
||||
"HeaderIgnoredFiles": "Ignored Files",
|
||||
"HeaderItemFiles": "Item Files",
|
||||
"HeaderLastListeningSession": "Last Listening Session",
|
||||
"HeaderLatestEpisodes": "Latest episodes",
|
||||
"HeaderLibraries": "Libraries",
|
||||
"HeaderLibraryFiles": "Library Files",
|
||||
"HeaderLibraryStats": "Library Stats",
|
||||
"HeaderYourStats": "Your Stats",
|
||||
"HeaderSettingsGeneral": "General",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderListeningSessions": "Listening Sessions",
|
||||
"HeaderListeningStats": "Listening Stats",
|
||||
"HeaderLogin": "Login",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderMatch": "Match",
|
||||
"HeaderMetadataToEmbed": "Metadata to embed",
|
||||
"HeaderNewAccount": "New Account",
|
||||
"HeaderNewLibrary": "New Library",
|
||||
"HeaderNotifications": "Notifications",
|
||||
"HeaderOpenRSSFeed": "Open RSS Feed",
|
||||
"HeaderOtherFiles": "Other Files",
|
||||
"HeaderPermissions": "Permissions",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasts to Add",
|
||||
"HeaderPreviewCover": "Preview Cover",
|
||||
"HeaderRemoveEpisode": "Remove Episode",
|
||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||
"HeaderSchedule": "Schedule",
|
||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSetBackupSchedule": "Set Backup Schedule",
|
||||
"HeaderSettings": "Settings",
|
||||
"HeaderSettingsDisplay": "Display",
|
||||
"HeaderSettingsExperimental": "Experimental Features",
|
||||
"LabelUsername": "Username",
|
||||
"HeaderSettingsGeneral": "General",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSleepTimer": "Sleep Timer",
|
||||
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
|
||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||
"HeaderStatsTop10Authors": "Top 10 Authors",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderUpdateAccount": "Update Account",
|
||||
"HeaderUpdateAuthor": "Update Author",
|
||||
"HeaderUpdateDetails": "Update Details",
|
||||
"HeaderUpdateLibrary": "Update Library",
|
||||
"HeaderUsers": "Users",
|
||||
"HeaderYourStats": "Your Stats",
|
||||
"LabelAccountType": "Account Type",
|
||||
"LabelPassword": "Password",
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelAccountTypeAdmin": "Admin",
|
||||
"LabelAccountTypeGuest": "Guest",
|
||||
"LabelAccountTypeUser": "User",
|
||||
"LabelActivity": "Activity",
|
||||
"LabelAddedAt": "Added At",
|
||||
"LabelAddToCollection": "Add to Collection",
|
||||
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "All",
|
||||
"LabelAllUsers": "All Users",
|
||||
"LabelAuthor": "Author",
|
||||
"LabelAuthorFirstLast": "Author (First Last)",
|
||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||
"LabelAuthors": "Authors",
|
||||
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
|
||||
"LabelBackToUser": "Back to User",
|
||||
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
|
||||
"LabelBackupsNumberToKeep": "Number of backups to keep",
|
||||
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
|
||||
"LabelBooks": "Books",
|
||||
"LabelChangePassword": "Change Password",
|
||||
"LabelChaptersFound": "chapters found",
|
||||
"LabelChapterTitle": "Chapter Title",
|
||||
"LabelClosePlayer": "Close player",
|
||||
"LabelCollapseSeries": "Collapse Series",
|
||||
"LabelCollections": "Collections",
|
||||
"LabelComplete": "Complete",
|
||||
"LabelConfirmPassword": "Confirm Password",
|
||||
"LabelContinueListening": "Continue Listening",
|
||||
"LabelContinueSeries": "Continue Series",
|
||||
"LabelCover": "Cover",
|
||||
"LabelCoverImageURL": "Cover Image URL",
|
||||
"LabelCreatedAt": "Created At",
|
||||
"LabelCronExpression": "Cron Expression",
|
||||
"LabelCurrent": "Current",
|
||||
"LabelCurrently": "Currently:",
|
||||
"LabelDatetime": "Datetime",
|
||||
"LabelDescription": "Description",
|
||||
"LabelDeselectAll": "Deselect All",
|
||||
"LabelDevice": "Device",
|
||||
"LabelDeviceInfo": "Device Info",
|
||||
"LabelDirectory": "Directory",
|
||||
"LabelDiscFromFilename": "Disc from Filename",
|
||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDuration": "Duration",
|
||||
"LabelDurationFound": "Duration found:",
|
||||
"LabelEdit": "Edit",
|
||||
"LabelEnable": "Enable",
|
||||
"LabelEnd": "End",
|
||||
"LabelEpisode": "Episode",
|
||||
"LabelEpisodeTitle": "Episode Title",
|
||||
"LabelEpisodeType": "Episode Type",
|
||||
"LabelExplicit": "Explicit",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "File",
|
||||
"LabelFileBirthtime": "File Birthtime",
|
||||
"LabelFileModified": "File Modified",
|
||||
"LabelFilename": "Filename",
|
||||
"LabelFilterByUser": "Filter by User",
|
||||
"LabelFindEpisodes": "Find Episodes",
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folders",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
"LabelHour": "Hour",
|
||||
"LabelIcon": "Icon",
|
||||
"LabelIncludeInTracklist": "Include in Tracklist",
|
||||
"LabelIncomplete": "Incomplete",
|
||||
"LabelInProgress": "In Progress",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
|
||||
"LabelIntervalEvery12Hours": "Every 12 hours",
|
||||
"LabelIntervalEvery15Minutes": "Every 15 minutes",
|
||||
"LabelIntervalEvery2Hours": "Every 2 hours",
|
||||
"LabelIntervalEvery30Minutes": "Every 30 minutes",
|
||||
"LabelIntervalEvery6Hours": "Every 6 hours",
|
||||
"LabelIntervalEveryDay": "Every day",
|
||||
"LabelIntervalEveryHour": "Every hour",
|
||||
"LabelInvalidParts": "Invalid Parts",
|
||||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Language",
|
||||
"LabelLanguageDefaultServer": "Default Server Language",
|
||||
"LabelLastSeen": "Last Seen",
|
||||
"LabelLastTime": "Last Time",
|
||||
"LabelLastUpdate": "Last Update",
|
||||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelListenAgain": "Listen Again",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Warn",
|
||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||
"LabelMarkSeries": "Mark Series",
|
||||
"LabelMediaPlayer": "Media Player",
|
||||
"LabelMediaType": "Media Type",
|
||||
"LabelMetadataProvider": "Metadata Provider",
|
||||
"LabelMetaTag": "Meta Tag",
|
||||
"LabelMinute": "Minute",
|
||||
"LabelMissing": "Missing",
|
||||
"LabelMissingParts": "Missing Parts",
|
||||
"LabelMore": "More",
|
||||
"LabelName": "Name",
|
||||
"LabelNarrator": "Narrator",
|
||||
"LabelNarrators": "Narrators",
|
||||
"LabelNew": "New",
|
||||
"LabelNewestAuthors": "Newest Authors",
|
||||
"LabelNewestEpisodes": "Newest Episodes",
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
"LabelNotificationAvailableVariables": "Available variables",
|
||||
"LabelNotificationBodyTemplate": "Body Template",
|
||||
"LabelNotificationEvent": "Notification Event",
|
||||
"LabelNotificationsMaxFailedAttempts": "Max failed attempts",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
|
||||
"LabelNotificationsMaxQueueSize": "Max queue size for notification events",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotStarted": "Not Started",
|
||||
"LabelNumberOfBooks": "Number of Books",
|
||||
"LabelNumberOfEpisodes": "# of Episodes",
|
||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||
"LabelPassword": "Password",
|
||||
"LabelPath": "Path",
|
||||
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
||||
"LabelPermissionsAccessAllTags": "Can Access All Tags",
|
||||
"LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
|
||||
"LabelPermissionsDelete": "Can Delete",
|
||||
"LabelPermissionsDownload": "Can Download",
|
||||
"LabelPermissionsUpdate": "Can Update",
|
||||
"LabelPermissionsUpload": "Can Upload",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||
"LabelProgress": "Progress",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||
"LabelRSSFeedURL": "RSS Feed URL",
|
||||
"LabelSearchTerm": "Search Term",
|
||||
"LabelSearchTitle": "Search Title",
|
||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||
"LabelSettingsDateFormat": "Date Format",
|
||||
"LabelSettingsDisableWatcher": "Disable Watcher",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
|
||||
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||
"LabelSettingsEnableEReader": "Enable e-reader for all users",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader is still a work in progress, but use this setting to open it up to all your users (or use the \"Experimental Features\" toggle just for use by you)",
|
||||
"LabelSettingsExperimentalFeatures": "Experimental features",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
||||
"LabelSettingsFindCovers": "Find covers",
|
||||
"LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically",
|
||||
"LabelSettingsParseSubtitles": "Parse subtitles",
|
||||
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
|
||||
"LabelSettingsPreferAudioMetadata": "Prefer audio metadata",
|
||||
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names",
|
||||
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
|
||||
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||
"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",
|
||||
"LabelSettingsStoreMetadataWithItem": "Store metadata with item",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||
"LabelSettingsDateFormat": "Date Format",
|
||||
"LabelSettingsParseSubtitles": "Parse subtitles",
|
||||
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
|
||||
"LabelSettingsFindCovers": "Find covers",
|
||||
"LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time",
|
||||
"LabelShowAll": "Show All",
|
||||
"LabelSize": "Size",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelStart": "Start",
|
||||
"LabelStarted": "Started",
|
||||
"LabelStartedAt": "Started At",
|
||||
"LabelStartTime": "Start Time",
|
||||
"LabelStatsAudioTracks": "Audio Tracks",
|
||||
"LabelStatsAuthors": "Authors",
|
||||
"LabelStatsBestDay": "Best Day",
|
||||
"LabelStatsDailyAverage": "Daily Average",
|
||||
"LabelStatsDays": "Days",
|
||||
"LabelStatsDaysListened": "Days Listened",
|
||||
"LabelStatsHours": "Hours",
|
||||
"LabelStatsInARow": "in a row",
|
||||
"LabelStatsItemsFinished": "Items Finished",
|
||||
"LabelStatsItemsInLibrary": "Items in Library",
|
||||
"LabelStatsMinutes": "minutes",
|
||||
"LabelStatsMinutesListening": "Minutes Listening",
|
||||
"LabelStatsOverallDays": "Overall Days",
|
||||
"LabelStatsOverallHours": "Overall Hours",
|
||||
"LabelStatsWeekListening": "Week Listening",
|
||||
"LabelSubtitle": "Subtitle",
|
||||
"LabelSupportedFileTypes": "Supported File Types",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
"LabelTimeListenedToday": "Time Listened Today",
|
||||
"LabelTimeRemaining": "{0} remaining",
|
||||
"LabelTimeToShift": "Time to shift in seconds",
|
||||
"LabelTitle": "Title",
|
||||
"LabelToolsEmbedMetadata": "Embed Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
||||
"LabelTotalDuration": "Total Duration",
|
||||
"LabelTotalTimeListened": "Total Time Listened",
|
||||
"LabelTrackFromFilename": "Track from Filename",
|
||||
"LabelTrackFromMetadata": "Track from Metadata",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Type",
|
||||
"LabelUnknown": "Unknown",
|
||||
"LabelUpdateCover": "Update Cover",
|
||||
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
||||
"LabelUpdatedAt": "Updated At",
|
||||
"LabelUpdateDetails": "Update Details",
|
||||
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
|
||||
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
||||
"LabelUploaderDropFiles": "Drop files",
|
||||
"LabelUseChapterTrack": "Use chapter track",
|
||||
"LabelUseFullTrack": "Use full track",
|
||||
"LabelUser": "User",
|
||||
"LabelUsername": "Username",
|
||||
"LabelValue": "Value",
|
||||
"LabelVersion": "Version",
|
||||
"LabelViewBookmarks": "View bookmarks",
|
||||
"LabelViewChapters": "View chapters",
|
||||
"LabelViewQueue": "View player queue",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Weekdays to run",
|
||||
"LabelYourAudiobookDuration": "Your audiobook duration",
|
||||
"LabelYourBookmarks": "Your Bookmarks",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Your Progress",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"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",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Downloading episode",
|
||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||
"MessageEmbedFinished": "Embed Finished!",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
||||
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
||||
"MessageFetching": "Fetching...",
|
||||
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
||||
"MessageImportantNotice": "Important Notice!",
|
||||
"MessageInsertChapterBelow": "Insert chapter below",
|
||||
"MessageItemsSelected": "{0} Items Selected",
|
||||
"MessageJoinUsOn": "Join us on",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
||||
"MessageLoading": "Loading...",
|
||||
"MessageLoadingFolders": "Loading folders...",
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAsFinished": "Mark as Finished",
|
||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||
"MessageNoAudioTracks": "No audio tracks",
|
||||
"MessageNoAuthors": "No Authors",
|
||||
"MessageNoBackups": "No Backups",
|
||||
"MessageNoBookmarks": "No Bookmarks",
|
||||
"MessageNoChapters": "No Chapters",
|
||||
"MessageNoCollections": "No Collections",
|
||||
"MessageNoCoversFound": "No Covers Found",
|
||||
"MessageNoDescription": "No description",
|
||||
"MessageNoEpisodeMatchesFound": "No episode matches found",
|
||||
"MessageNoEpisodes": "No Episodes",
|
||||
"MessageNoFoldersAvailable": "No Folders Available",
|
||||
"MessageNoGenres": "No Genres",
|
||||
"MessageNoIssues": "No Issues",
|
||||
"MessageNoItems": "No Items",
|
||||
"MessageNoItemsFound": "No items found",
|
||||
"MessageNoListeningSessions": "No Listening Sessions",
|
||||
"MessageNoLogs": "No Logs",
|
||||
"MessageNoMediaProgress": "No Media Progress",
|
||||
"MessageNoNotifications": "No Notifications",
|
||||
"MessageNoPodcastsFound": "No podcasts found",
|
||||
"MessageNoResults": "No Results",
|
||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Not yet implemented",
|
||||
"MessageNoUpdateNecessary": "No update necessary",
|
||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "or",
|
||||
"MessagePauseChapter": "Pause chapter playback",
|
||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||
"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.",
|
||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
||||
"MessageRemoveChapter": "Remove chapter",
|
||||
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
|
||||
"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",
|
||||
"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",
|
||||
"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}?",
|
||||
"MessageThinking": "Thinking...",
|
||||
"MessageUploaderItemFailed": "Failed to upload",
|
||||
"MessageUploaderItemSuccess": "Successfully Uploaded!",
|
||||
"MessageUploading": "Uploading...",
|
||||
"MessageValidCronExpression": "Valid cron expression",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
|
||||
"MessageXLibraryIsEmpty": "{0} Library is empty!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found",
|
||||
"MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found",
|
||||
"NoteChangeRootPassword": "Root user is the only user that can have an empty password",
|
||||
"SearchPlaceholder": "Search.."
|
||||
"NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
|
||||
"NoteFolderPicker": "Note: folders already mapped will not be shown",
|
||||
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
|
||||
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",
|
||||
"NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.",
|
||||
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
|
||||
"PlaceholderNewCollection": "New collection name",
|
||||
"PlaceholderNewFolderPath": "New folder path",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Search..",
|
||||
"ToastAccountUpdateFailed": "Failed to update account",
|
||||
"ToastAccountUpdateSuccess": "Account updated",
|
||||
"ToastAuthorImageRemoveFailed": "Failed to remove image",
|
||||
"ToastAuthorImageRemoveSuccess": "Author image removed",
|
||||
"ToastAuthorUpdateFailed": "Failed to update author",
|
||||
"ToastAuthorUpdateMerged": "Author merged",
|
||||
"ToastAuthorUpdateSuccess": "Author updated",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
|
||||
"ToastBackupCreateFailed": "Failed to create backup",
|
||||
"ToastBackupCreateSuccess": "Backup created",
|
||||
"ToastBackupDeleteFailed": "Failed to delete backup",
|
||||
"ToastBackupDeleteSuccess": "Backup deleted",
|
||||
"ToastBackupRestoreFailed": "Failed to restore backup",
|
||||
"ToastBackupUploadFailed": "Failed to upload backup",
|
||||
"ToastBackupUploadSuccess": "Backup uploaded",
|
||||
"ToastBatchUpdateFailed": "Batch update failed",
|
||||
"ToastBatchUpdateSuccess": "Batch update success",
|
||||
"ToastBookmarkCreateFailed": "Failed to create bookmark",
|
||||
"ToastBookmarkCreateSuccess": "Bookmark added",
|
||||
"ToastBookmarkRemoveFailed": "Failed to remove bookmark",
|
||||
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
||||
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
|
||||
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
|
||||
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
||||
"ToastCollectionRemoveFailed": "Failed to remove collection",
|
||||
"ToastCollectionRemoveSuccess": "Collection removed",
|
||||
"ToastCollectionUpdateFailed": "Failed to update collection",
|
||||
"ToastCollectionUpdateSuccess": "Collection updated",
|
||||
"ToastItemCoverUpdateFailed": "Failed to update item cover",
|
||||
"ToastItemCoverUpdateSuccess": "Item cover updated",
|
||||
"ToastItemDetailsUpdateFailed": "Failed to update item details",
|
||||
"ToastItemDetailsUpdateSuccess": "Item details updated",
|
||||
"ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
|
||||
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
|
||||
"ToastLibraryCreateFailed": "Failed to create library",
|
||||
"ToastLibraryCreateSuccess": "Library \"{0}\" created",
|
||||
"ToastLibraryDeleteFailed": "Failed to delete library",
|
||||
"ToastLibraryDeleteSuccess": "Library deleted",
|
||||
"ToastLibraryScanFailedToStart": "Failed to start scan",
|
||||
"ToastLibraryScanStarted": "Library scan started",
|
||||
"ToastLibraryUpdateFailed": "Failed to update library",
|
||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Failed to create podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast created successfully",
|
||||
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||
"ToastSessionDeleteSuccess": "Session deleted",
|
||||
"ToastSocketConnected": "Socket connected",
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Failed to delete user",
|
||||
"ToastUserDeleteSuccess": "User deleted",
|
||||
"WeekdayFriday": "Friday",
|
||||
"WeekdayMonday": "Monday",
|
||||
"WeekdaySaturday": "Saturday",
|
||||
"WeekdaySunday": "Sunday",
|
||||
"WeekdayThursday": "Thursday",
|
||||
"WeekdayTuesday": "Tuesday",
|
||||
"WeekdayWednesday": "Wednesday"
|
||||
}
|
||||
601
client/strings/fr.json
Normal file
601
client/strings/fr.json
Normal file
@@ -0,0 +1,601 @@
|
||||
{
|
||||
"ButtonAdd": "Ajouter",
|
||||
"ButtonAddChapters": "Ajouter Chapitre",
|
||||
"ButtonAddPodcasts": "Ajouter Podcasts",
|
||||
"ButtonAddYourFirstLibrary": "Ajouter votre Première Bibliothèque",
|
||||
"ButtonApply": "Appliquer",
|
||||
"ButtonApplyChapters": "Appliquer les Chapitres",
|
||||
"ButtonAuthors": "Auteurs",
|
||||
"ButtonBrowseForFolder": "Naviguer vers le Répertoire",
|
||||
"ButtonCancel": "Annuler",
|
||||
"ButtonCancelEncode": "Annuler l'encodage",
|
||||
"ButtonChangeRootPassword": "Changer le mot de passe Administrateur",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Vérifier & Télécharger de Nouveaux Episodes",
|
||||
"ButtonChooseAFolder": "Choisir un Dossier",
|
||||
"ButtonChooseFiles": "Choisir les Fichiers",
|
||||
"ButtonClearFilter": "Effacer le Filtre",
|
||||
"ButtonCloseFeed": "Fermer le Flux",
|
||||
"ButtonCollections": "Collections",
|
||||
"ButtonConfigureScanner": "Configurer le Scan",
|
||||
"ButtonCreate": "Créer",
|
||||
"ButtonCreateBackup": "Créer une Sauvegarde",
|
||||
"ButtonDelete": "Effacer",
|
||||
"ButtonEditChapters": "Editer Chapitre",
|
||||
"ButtonEditPodcast": "Editer Podcast",
|
||||
"ButtonForceReScan": "Forcer un Re-Scan",
|
||||
"ButtonFullPath": "Chemin Complet",
|
||||
"ButtonHide": "Cacher",
|
||||
"ButtonHome": "Accueil",
|
||||
"ButtonIssues": "Parutions",
|
||||
"ButtonLatest": "Dernière Version",
|
||||
"ButtonLibrary": "Bibliothèque",
|
||||
"ButtonLogout": "Se Déconnecter",
|
||||
"ButtonLookup": "Rechercher",
|
||||
"ButtonManageTracks": "Gérer les pistes",
|
||||
"ButtonMapChapterTitles": "Correspondance des titres de chapitres",
|
||||
"ButtonMatchAllAuthors": "Rechercher tous les Auteurs",
|
||||
"ButtonMatchBooks": "Rechercher les Livres",
|
||||
"ButtonNevermind": "Oubliez cela",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Ouvrir le Flux",
|
||||
"ButtonOpenManager": "Ouvrir le Gestionnaire",
|
||||
"ButtonPlay": "Ecouter",
|
||||
"ButtonPlaying": "En Lecture",
|
||||
"ButtonPlaylists": "Listes de Lecture",
|
||||
"ButtonPurgeAllCache": "Purger Tout le Cache",
|
||||
"ButtonPurgeItemsCache": "Purger le Cache des Articles",
|
||||
"ButtonPurgeMediaProgress": "Purger la Progression des Médias",
|
||||
"ButtonQueueAddItem": "Ajouter à la Liste de Lecture",
|
||||
"ButtonQueueRemoveItem": "Supprimer de la Liste de Lecture",
|
||||
"ButtonQuickMatch": "Recherche Rapide",
|
||||
"ButtonRead": "Lire",
|
||||
"ButtonRemove": "Supprimer",
|
||||
"ButtonRemoveAll": "Supprimer Tout",
|
||||
"ButtonRemoveAllLibraryItems": "Supprimer Tout les Articles de la Bibliothèque",
|
||||
"ButtonRemoveFromContinueListening": "Supprimer de Continuer à Ecouter",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Supprimer la Série de Continuer la Série",
|
||||
"ButtonReScan": "Re-Scan",
|
||||
"ButtonReset": "Réinitialiser",
|
||||
"ButtonRestore": "Rétablir",
|
||||
"ButtonSave": "Sauvegarder",
|
||||
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
||||
"ButtonSaveTracklist": "Sauvegarder la Tracklist",
|
||||
"ButtonScan": "Scanner",
|
||||
"ButtonScanLibrary": "Scanner la Bibliothèque",
|
||||
"ButtonSearch": "Rechercher",
|
||||
"ButtonSelectFolderPath": "Sélectionner le Chemin du Dossier",
|
||||
"ButtonSeries": "Séries",
|
||||
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
||||
"ButtonShiftTimes": "Décaler le Temps",
|
||||
"ButtonShow": "Montrer",
|
||||
"ButtonStartM4BEncode": "Démarrer l'Encodage M4B",
|
||||
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées Intégrées",
|
||||
"ButtonSubmit": "Soumettre",
|
||||
"ButtonUpload": "Téléverser",
|
||||
"ButtonUploadBackup": "Téléverser une Sauvegarde",
|
||||
"ButtonUploadCover": "Téléverser une Couverture",
|
||||
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
||||
"ButtonViewAll": "Voir Tout",
|
||||
"ButtonYes": "Oui",
|
||||
"HeaderAccount": "Compte",
|
||||
"HeaderAdvanced": "Avancé",
|
||||
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
|
||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||
"HeaderAudioTracks": "Pistes Audio",
|
||||
"HeaderBackups": "Sauvegardes",
|
||||
"HeaderChangePassword": "Chager le Mot de Passe",
|
||||
"HeaderChapters": "Chapitres",
|
||||
"HeaderChooseAFolder": "Choisir un Dossier",
|
||||
"HeaderCollection": "Collection",
|
||||
"HeaderCollectionItems": "Entrées de la Collection",
|
||||
"HeaderCover": "Couverture",
|
||||
"HeaderDetails": "Détails",
|
||||
"HeaderEpisodes": "Episodes",
|
||||
"HeaderFiles": "Fichiers",
|
||||
"HeaderFindChapters": "Trouver les Chapitres",
|
||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||
"HeaderItemFiles": "Fichiers des Articles",
|
||||
"HeaderLastListeningSession": "Dernière Session d'Ecoute",
|
||||
"HeaderLatestEpisodes": "Dernier Episodes",
|
||||
"HeaderLibraries": "Bibliothèque",
|
||||
"HeaderLibraryFiles": "Fichier de Bibliothèque",
|
||||
"HeaderLibraryStats": "Statistiques de Bibliothèque",
|
||||
"HeaderListeningSessions": "Sessions d'Ecoute",
|
||||
"HeaderListeningStats": "Statistiques d'Ecoute",
|
||||
"HeaderLogin": "Connexion",
|
||||
"HeaderLogs": "Fichiers Journaux",
|
||||
"HeaderMatch": "Rechercher",
|
||||
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
||||
"HeaderNewAccount": "Nouveau Compte",
|
||||
"HeaderNewLibrary": "Nouvelle Bibliothèque",
|
||||
"HeaderNotifications": "Notifications",
|
||||
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
|
||||
"HeaderOtherFiles": "Autres Fichiers",
|
||||
"HeaderPermissions": "Permissions",
|
||||
"HeaderPlayerQueue": "Liste d'Ecoute",
|
||||
"HeaderPlaylist": "Liste de Lecture",
|
||||
"HeaderPlaylistItems": "Elements de la Liste de Lecture",
|
||||
"HeaderPodcastsToAdd": "Podcasts à Ajouter",
|
||||
"HeaderPreviewCover": "Prévisualiser la Couverture",
|
||||
"HeaderRemoveEpisode": "Supprimer l'Episode",
|
||||
"HeaderRemoveEpisodes": "Suppression de {0} Episodes",
|
||||
"HeaderRSSFeedIsOpen": "Le Flux RSS et Ouvert",
|
||||
"HeaderSavedMediaProgress": "Progression de la Sauvegarde des Médias",
|
||||
"HeaderSchedule": "Programmation",
|
||||
"HeaderScheduleLibraryScans": "Scan Automatique de la Bibliothèque",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSetBackupSchedule": "Activer la Sauvegarde Automatique",
|
||||
"HeaderSettings": "Paramètres",
|
||||
"HeaderSettingsDisplay": "Affichage",
|
||||
"HeaderSettingsExperimental": "Fonctionnalités Expérimentales",
|
||||
"HeaderSettingsGeneral": "Général",
|
||||
"HeaderSettingsScanner": "Scanneur",
|
||||
"HeaderSleepTimer": "Minuterie",
|
||||
"HeaderStatsLongestItems": "Articles les Plus Long (heures)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutes d'Ecoute (7 derniers jours)",
|
||||
"HeaderStatsRecentSessions": "Sessions Récentes",
|
||||
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTools": "Outils",
|
||||
"HeaderUpdateAccount": "Mettre à jour le Compte",
|
||||
"HeaderUpdateAuthor": "Mettre à jour l'Auteur",
|
||||
"HeaderUpdateDetails": "Mettre à jour les Détails",
|
||||
"HeaderUpdateLibrary": "Mettre à jour la Bibliothèque",
|
||||
"HeaderUsers": "Utilisateurs",
|
||||
"HeaderYourStats": "Vos Statistiques",
|
||||
"LabelAccountType": "Type de Compte",
|
||||
"LabelAccountTypeAdmin": "Admin",
|
||||
"LabelAccountTypeGuest": "Invité",
|
||||
"LabelAccountTypeUser": "Utilisateur",
|
||||
"LabelActivity": "Activité",
|
||||
"LabelAddedAt": "Date d'Ajout",
|
||||
"LabelAddToCollection": "Ajouter à la Collection",
|
||||
"LabelAddToCollectionBatch": "Ajout de {0} Livres à la Collection",
|
||||
"LabelAddToPlaylist": "Ajouter à la Liste de Lecture",
|
||||
"LabelAddToPlaylistBatch": "{0} Elements Ajoutés à la Liste de Lecture",
|
||||
"LabelAll": "Tout",
|
||||
"LabelAllUsers": "Tous les Utilisateurs",
|
||||
"LabelAuthor": "Auteur",
|
||||
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
||||
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
||||
"LabelAuthors": "Auteurs",
|
||||
"LabelAutoDownloadEpisodes": "Téléchargement Automatique d'Episode",
|
||||
"LabelBackToUser": "Revenir à l'Utilisateur",
|
||||
"LabelBackupsEnableAutomaticBackups": "Activer les Sauvegardes Automatiques",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Taille de Sauvegarde Maximale (en GB)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
|
||||
"LabelBackupsNumberToKeep": "Nombre de Sauvegardes à maintenir",
|
||||
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
|
||||
"LabelBooks": "Livres",
|
||||
"LabelChangePassword": "Changer le Mot de Passe",
|
||||
"LabelChaptersFound": "Chapitres Trouvés",
|
||||
"LabelChapterTitle": "Titres du Chapitre",
|
||||
"LabelClosePlayer": "Fermer le Lecteur",
|
||||
"LabelCollapseSeries": "Réduire les Séries",
|
||||
"LabelCollections": "Collections",
|
||||
"LabelComplete": "Complet",
|
||||
"LabelConfirmPassword": "Confirmer le Mot de Passe",
|
||||
"LabelContinueListening": "Continuer la Lecture",
|
||||
"LabelContinueSeries": "Continuer la Série",
|
||||
"LabelCover": "Couverture",
|
||||
"LabelCoverImageURL": "URL vers l'image de Couverture",
|
||||
"LabelCreatedAt": "Créé le",
|
||||
"LabelCronExpression": "Expression Cron",
|
||||
"LabelCurrent": "Courrant",
|
||||
"LabelCurrently": "En ce Moment:",
|
||||
"LabelDatetime": "Datetime",
|
||||
"LabelDescription": "Description",
|
||||
"LabelDeselectAll": "Tout Déselectionner",
|
||||
"LabelDevice": "Appareil",
|
||||
"LabelDeviceInfo": "Détail de l'Appareil",
|
||||
"LabelDirectory": "Répertoire",
|
||||
"LabelDiscFromFilename": "Disque depuis le Fichier",
|
||||
"LabelDiscFromMetadata": "Disque depuis les Métadonnées",
|
||||
"LabelDownload": "Téléchargement",
|
||||
"LabelDuration": "Durée",
|
||||
"LabelDurationFound": "Durée Trouvée:",
|
||||
"LabelEdit": "Editer",
|
||||
"LabelEnable": "Activer",
|
||||
"LabelEnd": "Fin",
|
||||
"LabelEpisode": "Episode",
|
||||
"LabelEpisodeTitle": "Titre de l'Episode",
|
||||
"LabelEpisodeType": "Type de l'Episode",
|
||||
"LabelExplicit": "Restriction",
|
||||
"LabelFeedURL": "URL de Flux",
|
||||
"LabelFile": "Fichier",
|
||||
"LabelFileBirthtime": "Creation du Fichier",
|
||||
"LabelFileModified": "Modification du Fichier",
|
||||
"LabelFilename": "Nom de Fichier",
|
||||
"LabelFilterByUser": "Filtrer par l'Utilisateur",
|
||||
"LabelFindEpisodes": "Trouver des Episodes",
|
||||
"LabelFinished": "Fini(e)",
|
||||
"LabelFolder": "Dossier",
|
||||
"LabelFolders": "Dossiers",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Effacement du Fichier",
|
||||
"LabelHour": "Heure",
|
||||
"LabelIcon": "Icone",
|
||||
"LabelIncludeInTracklist": "Inclure dans la Liste des Pistes",
|
||||
"LabelIncomplete": "Incomplet",
|
||||
"LabelInProgress": "En Cours",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Journalier/Hebdomadaire Personnalisé",
|
||||
"LabelIntervalEvery12Hours": "Toutes les 12 heures",
|
||||
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
|
||||
"LabelIntervalEvery2Hours": "Toutes les 2 heures",
|
||||
"LabelIntervalEvery30Minutes": "Toutes les 30 minutes",
|
||||
"LabelIntervalEvery6Hours": "Toutes les 6 heures",
|
||||
"LabelIntervalEveryDay": "Tous les jours",
|
||||
"LabelIntervalEveryHour": "Toutes les heures",
|
||||
"LabelInvalidParts": "Parties Invalides",
|
||||
"LabelItem": "Article",
|
||||
"LabelLanguage": "Langue",
|
||||
"LabelLanguageDefaultServer": "Langue par Défaut",
|
||||
"LabelLastSeen": "Vu Dernièrement",
|
||||
"LabelLastTime": "Progression",
|
||||
"LabelLastUpdate": "Dernière Mise à Jour",
|
||||
"LabelLess": "Moins",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliothèque Accessible à l'Utilisateur",
|
||||
"LabelLibrary": "Bibliothèque",
|
||||
"LabelLibraryItem": "Article de Bibliothèque",
|
||||
"LabelLibraryName": "Nom de Bibliothèque",
|
||||
"LabelLimit": "Limite",
|
||||
"LabelListenAgain": "Ecouter à Nouveau",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Warn",
|
||||
"LabelLookForNewEpisodesAfterDate": "Rechercher de Nouveaux Episode après cette Date",
|
||||
"LabelMarkSeries": "Marquer la Série",
|
||||
"LabelMediaPlayer": "Lecteur Multimédia",
|
||||
"LabelMediaType": "Type de Média",
|
||||
"LabelMetadataProvider": "Fournisseur de Métadonnées",
|
||||
"LabelMetaTag": "Etiquette de Métadonnée",
|
||||
"LabelMinute": "Minute",
|
||||
"LabelMissing": "Manquant",
|
||||
"LabelMissingParts": "Parties Manquantes",
|
||||
"LabelMore": "Plus",
|
||||
"LabelName": "Nom",
|
||||
"LabelNarrator": "Narrateur",
|
||||
"LabelNarrators": "Narrateurs",
|
||||
"LabelNew": "Nouveau",
|
||||
"LabelNewestAuthors": "Nouveaux Auteurs",
|
||||
"LabelNewestEpisodes": "Derniers Episodes",
|
||||
"LabelNewPassword": "Nouveau Mot de Passe",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Non Terminé(e)",
|
||||
"LabelNotificationAppriseURL": "URL(s) d'Apprise",
|
||||
"LabelNotificationAvailableVariables": "Variables Disponibles",
|
||||
"LabelNotificationBodyTemplate": "Modèle de Message",
|
||||
"LabelNotificationEvent": "Evènement de Notification",
|
||||
"LabelNotificationsMaxFailedAttempts": "Nombres de Tentatives d'Envoi",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
||||
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Le notification seront ignorées si la file d'attente est à son maximum. Cela empêche un flot trop important.",
|
||||
"LabelNotificationTitleTemplate": "Modèle de Titre",
|
||||
"LabelNotStarted": "Non Démarré(e)",
|
||||
"LabelNumberOfBooks": "Nombre de Livres",
|
||||
"LabelNumberOfEpisodes": "Nombre d'Episodes",
|
||||
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
||||
"LabelPassword": "Mot de Passe",
|
||||
"LabelPath": "Chemin",
|
||||
"LabelPermissionsAccessAllLibraries": "Peut Acceder à Toutes les Bibliothèque",
|
||||
"LabelPermissionsAccessAllTags": "Peut Acceder à Toutes les Etiquettes",
|
||||
"LabelPermissionsAccessExplicitContent": "Peut Acceter au Contenu Restreint",
|
||||
"LabelPermissionsDelete": "Peut Supprimer",
|
||||
"LabelPermissionsDownload": "Peut Télécharger",
|
||||
"LabelPermissionsUpdate": "Peut Mettre à Jour",
|
||||
"LabelPermissionsUpload": "Peut Téléverser",
|
||||
"LabelPhotoPathURL": "Chemin/URL des photos",
|
||||
"LabelPlaylists": "Listes de Lecture",
|
||||
"LabelPlayMethod": "Méthode d'Ecoute",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||
"LabelProgress": "Progression",
|
||||
"LabelProvider": "Fournisseur",
|
||||
"LabelPubDate": "Date de Publication",
|
||||
"LabelPublisher": "Editeur",
|
||||
"LabelPublishYear": "Année d'Edition",
|
||||
"LabelRecentlyAdded": "Derniers Ajouts",
|
||||
"LabelRecentSeries": "Séries Récentes",
|
||||
"LabelRegion": "Région",
|
||||
"LabelReleaseDate": "Date de Parution",
|
||||
"LabelRemoveCover": "Supprimer la Couverture",
|
||||
"LabelRSSFeedOpen": "Flux RSS Ouvert",
|
||||
"LabelRSSFeedSlug": "Flux RSS Slug",
|
||||
"LabelRSSFeedURL": "URL du Flux RSS",
|
||||
"LabelSearchTerm": "Terme de Recherche",
|
||||
"LabelSearchTitle": "Titre de Recherche",
|
||||
"LabelSearchTitleOrASIN": "Recherche du Titre ou ASIN",
|
||||
"LabelSeason": "Saison",
|
||||
"LabelSequence": "Séquence",
|
||||
"LabelSeries": "Séries",
|
||||
"LabelSeriesName": "Nom de la Série",
|
||||
"LabelSeriesProgress": "Progression de Séries",
|
||||
"LabelSettingsBookshelfViewHelp": "Design Skeumorphic avec une Etagère en Bois",
|
||||
"LabelSettingsChromecastSupport": "Support Chromecast",
|
||||
"LabelSettingsDateFormat": "Format de Date",
|
||||
"LabelSettingsDisableWatcher": "Désactiver la Surveillance",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance du dossier pour la Bibliothèque",
|
||||
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
||||
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre l'active pour tous les utilisateurs (ou utiliser l'interrupteur \"Fonctionnalités Expérimentales\" pour l'activer seulement pour vous)",
|
||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités Expérimentales",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquels nous attendons votre retour et expérience. Cliquer pour ouvrir la discussion Github.",
|
||||
"LabelSettingsFindCovers": "Rechercher des Couvertures",
|
||||
"LabelSettingsFindCoversHelp": "Si votre Livre Audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, le scanner tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps de scan.",
|
||||
"LabelSettingsHomePageBookshelfView": "La page d'Accueil utilise la vue étagère",
|
||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
||||
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 d'Overdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
|
||||
"LabelSettingsParseSubtitles": "Analyse des Sous-titres",
|
||||
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par \" - \"<br>i.e. \"Titre du Livre - Ceci est un sous-titre\" aura le sous-titre \"Ceci est un sous-titre\"",
|
||||
"LabelSettingsPreferAudioMetadata": "Préférer les Métadonnées Audio",
|
||||
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
|
||||
"LabelSettingsPreferMatchedMetadata": "Préférer les Métadonnées par correspondance",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l'article lors d'une Recherche par Correspondance Rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
||||
"LabelSettingsPreferOPFMetadata": "Préférer les Métadonnées OPF",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "Les fichiers de métadonnées OPF seront utilisés à la place des noms de dossier pour les détails du Livre Audio",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe \"le\", le livre avec pour titre \"Le Titre du Livre\" sera trié en tant que \"Titre du Livre, Le\"",
|
||||
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
|
||||
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standardes de 1.6:1.",
|
||||
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiersde l'article. Seul un fichier nommé \"cover\" sera gardé.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l'article avec une extension \".abs\".",
|
||||
"LabelShowAll": "Afficher Tout",
|
||||
"LabelSize": "Taille",
|
||||
"LabelSleepTimer": "Minuterie",
|
||||
"LabelStart": "Démarrer",
|
||||
"LabelStarted": "Démarré",
|
||||
"LabelStartedAt": "Démarré à",
|
||||
"LabelStartTime": "Heure de Démarrage",
|
||||
"LabelStatsAudioTracks": "Pistes Audios",
|
||||
"LabelStatsAuthors": "Auteurs",
|
||||
"LabelStatsBestDay": "Meilleur Jour",
|
||||
"LabelStatsDailyAverage": "Moyenne Journalière",
|
||||
"LabelStatsDays": "Jours",
|
||||
"LabelStatsDaysListened": "Jours d'écoute",
|
||||
"LabelStatsHours": "Heures",
|
||||
"LabelStatsInARow": "d'affilé(s)",
|
||||
"LabelStatsItemsFinished": "Articles Terminés",
|
||||
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
|
||||
"LabelStatsMinutes": "minutes",
|
||||
"LabelStatsMinutesListening": "Minutes d'écoute",
|
||||
"LabelStatsOverallDays": "Jours au total",
|
||||
"LabelStatsOverallHours": "Heures au total",
|
||||
"LabelStatsWeekListening": "Ecoute de la Semaine",
|
||||
"LabelSubtitle": "Sous-Titre",
|
||||
"LabelSupportedFileTypes": "Types de Fichiers Supportés",
|
||||
"LabelTag": "Etiquette",
|
||||
"LabelTags": "Etiquettes",
|
||||
"LabelTagsAccessibleToUser": "Etiquettes Accessibles à l'Utilisateur",
|
||||
"LabelTimeListened": "Temps d'écoute",
|
||||
"LabelTimeListenedToday": "Nombres d'écoutes Aujourd'hui",
|
||||
"LabelTimeRemaining": "{0} restantes",
|
||||
"LabelTimeToShift": "Temps de décalage en secondes",
|
||||
"LabelTitle": "Titre",
|
||||
"LabelToolsEmbedMetadata": "Métadonnées Intégrées",
|
||||
"LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.",
|
||||
"LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B",
|
||||
"LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.",
|
||||
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
|
||||
"LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, l'image de couverture et les chapitres.",
|
||||
"LabelTotalDuration": "Durée Totale",
|
||||
"LabelTotalTimeListened": "Temps d'Ecoute Total",
|
||||
"LabelTrackFromFilename": "Piste depuis le Fichier",
|
||||
"LabelTrackFromMetadata": "Piste depuis les Métadonnées",
|
||||
"LabelTracks": "Pistes",
|
||||
"LabelTracksMultiTrack": "Piste Multiple",
|
||||
"LabelTracksSingleTrack": "Piste Simple",
|
||||
"LabelType": "Type",
|
||||
"LabelUnknown": "Inconnu",
|
||||
"LabelUpdateCover": "Mettre à jour la Couverture",
|
||||
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu'une correspondance est trouvée",
|
||||
"LabelUpdatedAt": "Mis à jour à",
|
||||
"LabelUpdateDetails": "Mettre à jours les Détails",
|
||||
"LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsqu'une correspondance est trouvée",
|
||||
"LabelUploaderDragAndDrop": "Glisser & Déposer des Fichiers ou Dossiers",
|
||||
"LabelUploaderDropFiles": "Déposer des Fichiers",
|
||||
"LabelUseChapterTrack": "Utiliser la Piste du Chapitre",
|
||||
"LabelUseFullTrack": "Utiliser la Piste Complète",
|
||||
"LabelUser": "Utilisateur",
|
||||
"LabelUsername": "Nom d'Utilisateur",
|
||||
"LabelValue": "Valeur",
|
||||
"LabelVersion": "Version",
|
||||
"LabelViewBookmarks": "Voir les Signets",
|
||||
"LabelViewChapters": "Voir les Chapitres",
|
||||
"LabelViewQueue": "Voir la Liste de Lecture",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
||||
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
||||
"LabelYourBookmarks": "Vos Signets",
|
||||
"LabelYourPlaylists": "Vos Listes de Lecture",
|
||||
"LabelYourProgress": "Votre Progression",
|
||||
"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>.",
|
||||
"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.",
|
||||
"MessageBookshelfNoCollections": "Vous n'avez pas encore de collections",
|
||||
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n'est ouvert",
|
||||
"MessageBookshelfNoSeries": "Vous n'avez aucune séries",
|
||||
"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",
|
||||
"MessageCheckingCron": "Vérification du cron...",
|
||||
"MessageConfirmDeleteBackup": "Etes vous certain de vouloir supprimer la Sauvegarde de {0}?",
|
||||
"MessageConfirmDeleteLibrary": "Etes vous certain de vouloir supprimer définitivement la bibliothèque \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Etes vous certain de vouloir supprimer cette session?",
|
||||
"MessageConfirmForceReScan": "Etes vous certain de vouloir lancer une Analyse Forcée?",
|
||||
"MessageConfirmRemoveCollection": "Etes vous certain de vouloir supprimer la collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
||||
"MessageConfirmRemovePlaylist": "Etes vous certain de vouloir supprimer la liste de lecture \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Téléchargement de l'épisode",
|
||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l'ordre correct",
|
||||
"MessageEmbedFinished": "Intégration Terminée!",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) mis en file pour téléchargement",
|
||||
"MessageFeedURLWillBe": "L'URL du Flux sera {0}",
|
||||
"MessageFetching": "Récupération...",
|
||||
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, Fichiers OPF, et les fichiers textes seront analysés comme s'ils étaient nouveaux.",
|
||||
"MessageImportantNotice": "Information Importante!",
|
||||
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
||||
"MessageItemsSelected": "{0} Articles Sélectionnés",
|
||||
"MessageJoinUsOn": "Rejoignez-nous sur",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d'écoute l'an dernier",
|
||||
"MessageLoading": "Chargement...",
|
||||
"MessageLoadingFolders": "Chargement des Dossiers...",
|
||||
"MessageM4BFailed": "M4B en échec!",
|
||||
"MessageM4BFinished": "M4B terminé!",
|
||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre Livre Audio sans ajuster l'horodatage.",
|
||||
"MessageMarkAsFinished": "Marquer comme Terminé",
|
||||
"MessageMarkAsNotFinished": "Marquer comme non Terminé",
|
||||
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N'écrase pas les données existantes.",
|
||||
"MessageNoAudioTracks": "Pas de pistes audio",
|
||||
"MessageNoAuthors": "Pas d'Auteurs",
|
||||
"MessageNoBackups": "Pas de Sauvegardes",
|
||||
"MessageNoBookmarks": "Pas de Signets",
|
||||
"MessageNoChapters": "Pas de Chapitres",
|
||||
"MessageNoCollections": "Pas de Collections",
|
||||
"MessageNoCoversFound": "Pas de Couvertures Trouvées",
|
||||
"MessageNoDescription": "Pas de Description",
|
||||
"MessageNoEpisodeMatchesFound": "Pas de correspondance d'épisode trouvée",
|
||||
"MessageNoEpisodes": "Pas d'Episodes",
|
||||
"MessageNoFoldersAvailable": "Pas de Dossiers Disponibles",
|
||||
"MessageNoGenres": "Pas de Genres",
|
||||
"MessageNoIssues": "Pas de Parution",
|
||||
"MessageNoItems": "Pas d'Articles",
|
||||
"MessageNoItemsFound": "Pas d'Articles Trouvés",
|
||||
"MessageNoListeningSessions": "Pas de Sessions d'Ecoutes",
|
||||
"MessageNoLogs": "Pas de Journaux",
|
||||
"MessageNoMediaProgress": "Pas de Média en cours",
|
||||
"MessageNoNotifications": "Pas de Notifications",
|
||||
"MessageNoPodcastsFound": "Pas de podcasts trouvés",
|
||||
"MessageNoResults": "Pas de Résultats",
|
||||
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
||||
"MessageNoSeries": "Pas de Séries",
|
||||
"MessageNotYetImplemented": "Non implémenté",
|
||||
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
||||
"MessageNoUserPlaylists": "Vous n'avez aucune liste de lecture",
|
||||
"MessageOr": "ou",
|
||||
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
||||
"MessagePlayChapter": "Ecouter depuis le début du chapitre",
|
||||
"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é.",
|
||||
"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",
|
||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d'écoute",
|
||||
"MessageRemoveUserWarning": "Etes-vous certain de vouloir supprimer définitivement l'utilisateur \"{0}\"?",
|
||||
"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",
|
||||
"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",
|
||||
"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}?",
|
||||
"MessageThinking": "On Réfléchit...",
|
||||
"MessageUploaderItemFailed": "Echec du téléversement",
|
||||
"MessageUploaderItemSuccess": "Téléversement effectué!",
|
||||
"MessageUploading": "Téléversement...",
|
||||
"MessageValidCronExpression": "Expression cron valide",
|
||||
"MessageWatcherIsDisabledGlobally": "La Surveillance est désactivée par un paramètre global du serveur",
|
||||
"MessageXLibraryIsEmpty": "La Bibliothèque {0} est vide!",
|
||||
"MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée",
|
||||
"MessageYourAudiobookDurationIsShorter": "La durée de votre Livre Audio est plus courte que la durée trouvée",
|
||||
"NoteChangeRootPassword": "L'utilisateur Root est le seul a pouvoir utiliser un mote de passe vide",
|
||||
"NoteChapterEditorTimes": "Information: L'horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du Livre Audio.",
|
||||
"NoteFolderPicker": "Information: Les dossiers déjà surveillés ne sont pas affichés",
|
||||
"NoteFolderPickerDebian": "Information: La sélection de dossier sur une installation debian n'est pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.",
|
||||
"NoteRSSFeedPodcastAppsHttps": "Attention: La majorité des application de podcast nécessite une URL de flux en HTTPS.",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Warning: Un ou plus de vos épisodes ne possèdent pas de Pub Date. Certaines applications de podcast le requièrent.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers avec des fichiers médias seront traités en tant qu'articles séparés.",
|
||||
"NoteUploaderOnlyAudioFiles": "En téléversant seulement des fichiers audio, chaque fichier sera traité comme un Livre Audio séparé.",
|
||||
"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",
|
||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||
"PlaceholderSearch": "Recherche...",
|
||||
"ToastAccountUpdateFailed": "Echec de la mise à jour du compte",
|
||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||
"ToastAuthorImageRemoveFailed": "Echec de la suppression de l'image",
|
||||
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
||||
"ToastAuthorUpdateFailed": "Echec de la mise à jour de l'auteur",
|
||||
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
||||
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
||||
"ToastBackupCreateFailed": "Echec de la création de sauvegarde",
|
||||
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
||||
"ToastBackupDeleteFailed": "Echec de la suppression de sauvegarde",
|
||||
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
|
||||
"ToastBackupRestoreFailed": "Echec de la restauration de sauvegarde",
|
||||
"ToastBackupUploadFailed": "Echec du téléversement de sauvegarde",
|
||||
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
|
||||
"ToastBatchUpdateFailed": "Echec de la mise à jour par lot",
|
||||
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
|
||||
"ToastBookmarkCreateFailed": "Echec de la création de signet",
|
||||
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
||||
"ToastBookmarkRemoveFailed": "Echec de la suppression de signet",
|
||||
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
||||
"ToastBookmarkUpdateFailed": "Echec de la mise à jour de signet",
|
||||
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
||||
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
||||
"ToastCollectionItemsRemoveFailed": "Echec de la suppression de(s) article(s) de la collection",
|
||||
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
||||
"ToastCollectionRemoveFailed": "Echec de la suppression de la collection",
|
||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||
"ToastCollectionUpdateFailed": "Echec de la mise à jour de la collection",
|
||||
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
||||
"ToastItemCoverUpdateFailed": "Echec de la mise à jour de la couverture de l'article",
|
||||
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
||||
"ToastItemDetailsUpdateFailed": "Echec de la mise à jour des détails de l'article",
|
||||
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
||||
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
||||
"ToastItemMarkedAsFinishedFailed": "Echec de l'annotation terminée",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Echec de l'annotation non-terminée",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
||||
"ToastLibraryCreateFailed": "Echec de la création de bibliothèque",
|
||||
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
||||
"ToastLibraryDeleteFailed": "Echec de la suppression de la bibliothèque",
|
||||
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
||||
"ToastLibraryScanFailedToStart": "Echec du démarrage de l'analyse",
|
||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||
"ToastLibraryUpdateFailed": "Echec de la mise à jour de la bibliothèque",
|
||||
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
||||
"ToastPlaylistRemoveFailed": "Echec de la suppression de la liste de lecture",
|
||||
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
||||
"ToastPlaylistUpdateFailed": "Echec de la mise à jour de la liste de lecture",
|
||||
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
||||
"ToastPodcastCreateFailed": "Echec de la création du Podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast créé",
|
||||
"ToastRemoveItemFromCollectionFailed": "Echec de la suppression de l'article de la collection",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||
"ToastRSSFeedCloseFailed": "Echec de la fermeture du flux RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||
"ToastSessionDeleteFailed": "Echec de la suppression de session",
|
||||
"ToastSessionDeleteSuccess": "Session supprimée",
|
||||
"ToastSocketConnected": "WebSocket connectée",
|
||||
"ToastSocketDisconnected": "WebSocket déconnectée",
|
||||
"ToastSocketFailedToConnect": "Echec de la connexion WebSocket",
|
||||
"ToastUserDeleteFailed": "Echec de la suppression de l'utilisateur",
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé",
|
||||
"WeekdayFriday": "Vendredi",
|
||||
"WeekdayMonday": "Lundi",
|
||||
"WeekdaySaturday": "Samedi",
|
||||
"WeekdaySunday": "Dimanche",
|
||||
"WeekdayThursday": "Jeudi",
|
||||
"WeekdayTuesday": "Mardi",
|
||||
"WeekdayWednesday": "Mercredi"
|
||||
}
|
||||
@@ -13,8 +13,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove epizode",
|
||||
"ButtonChooseAFolder": "Odaberi folder",
|
||||
"ButtonChooseFiles": "Odaberi datoteke",
|
||||
"ButtonClearFilter": "Clear Filter",
|
||||
"ButtonCloseFeed": "Zatvori feed",
|
||||
"ButtonCollections": "Kolekcije",
|
||||
"ButtonConfigureScanner": "Configure Scanner",
|
||||
"ButtonCreate": "Napravi",
|
||||
"ButtonCreateBackup": "Napravi backup",
|
||||
"ButtonDelete": "Obriši",
|
||||
@@ -26,9 +28,9 @@
|
||||
"ButtonHome": "Početna stranica",
|
||||
"ButtonIssues": "Problemi",
|
||||
"ButtonLatest": "Najnovije",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonLogout": "Odjavi se",
|
||||
"ButtonLookup": "Potraži",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonManageTracks": "Upravljanje pjesmama",
|
||||
"ButtonMapChapterTitles": "Mapiraj imena poglavlja",
|
||||
"ButtonMatchAllAuthors": "Matchaj sve autore",
|
||||
@@ -39,6 +41,7 @@
|
||||
"ButtonOpenManager": "Otvori menadžera",
|
||||
"ButtonPlay": "Pokreni",
|
||||
"ButtonPlaying": "Playing",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Isprazni sav cache",
|
||||
"ButtonPurgeItemsCache": "Isprazni Items Cache",
|
||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||
@@ -58,9 +61,11 @@
|
||||
"ButtonSaveAndClose": "Spremi i zatvori",
|
||||
"ButtonSaveTracklist": "Spremi popis pjesama",
|
||||
"ButtonScan": "Skeniraj",
|
||||
"ButtonScanLibrary": "Scan Library",
|
||||
"ButtonSearch": "Traži",
|
||||
"ButtonSelectFolderPath": "Odaberi putanju do folder",
|
||||
"ButtonSeries": "Serije",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Pomakni vremena",
|
||||
"ButtonShow": "Prikaži",
|
||||
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
||||
@@ -104,10 +109,12 @@
|
||||
"HeaderNewAccount": "Novi korisnički račun",
|
||||
"HeaderNewLibrary": "Nova biblioteka",
|
||||
"HeaderNotifications": "Obavijesti",
|
||||
"HeaderOtherFiles": "Druge datoteke",
|
||||
"HeaderOpenRSSFeed": "Otvori RSS Feed",
|
||||
"HeaderOtherFiles": "Druge datoteke",
|
||||
"HeaderPermissions": "Dozvole",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasti za dodati",
|
||||
"HeaderPreviewCover": "Pregledaj Cover",
|
||||
"HeaderRemoveEpisode": "Ukloni epizodu",
|
||||
@@ -141,10 +148,16 @@
|
||||
"LabelAccountTypeGuest": "Gost",
|
||||
"LabelAccountTypeUser": "Korisnik",
|
||||
"LabelActivity": "Aktivnost",
|
||||
"LabelAddedAt": "Added At",
|
||||
"LabelAddToCollection": "Dodaj u kolekciju",
|
||||
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "All",
|
||||
"LabelAllUsers": "Svi korisnici",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Author (First Last)",
|
||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||
"LabelAuthors": "Autori",
|
||||
"LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
|
||||
"LabelBackToUser": "Nazad k korisniku",
|
||||
@@ -158,6 +171,7 @@
|
||||
"LabelChangePassword": "Promijeni lozinku",
|
||||
"LabelChaptersFound": "poglavlja pronađena",
|
||||
"LabelChapterTitle": "Ime poglavlja",
|
||||
"LabelClosePlayer": "Close player",
|
||||
"LabelCollapseSeries": "Collapse Series",
|
||||
"LabelCollections": "Kolekcije",
|
||||
"LabelComplete": "Complete",
|
||||
@@ -190,12 +204,15 @@
|
||||
"LabelExplicit": "Explicit",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "Datoteka",
|
||||
"LabelFileBirthtime": "File Birthtime",
|
||||
"LabelFileModified": "File Modified",
|
||||
"LabelFilename": "Ime datoteke",
|
||||
"LabelFilterByUser": "Filtriraj po korisniku",
|
||||
"LabelFindEpisodes": "Pronađi epizode",
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folderi",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Žanrovi",
|
||||
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
|
||||
"LabelHour": "Sat",
|
||||
@@ -204,6 +221,14 @@
|
||||
"LabelIncomplete": "Nepotpuno",
|
||||
"LabelInProgress": "U tijeku",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Custom daily/weekly",
|
||||
"LabelIntervalEvery12Hours": "Every 12 hours",
|
||||
"LabelIntervalEvery15Minutes": "Every 15 minutes",
|
||||
"LabelIntervalEvery2Hours": "Every 2 hours",
|
||||
"LabelIntervalEvery30Minutes": "Every 30 minutes",
|
||||
"LabelIntervalEvery6Hours": "Every 6 hours",
|
||||
"LabelIntervalEveryDay": "Every day",
|
||||
"LabelIntervalEveryHour": "Every hour",
|
||||
"LabelInvalidParts": "Nevaljajuči dijelovi",
|
||||
"LabelItem": "Stavka",
|
||||
"LabelLanguage": "Jezik",
|
||||
@@ -218,6 +243,9 @@
|
||||
"LabelLibraryName": "Ime biblioteke",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelListenAgain": "Slušaj ponovno",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Warn",
|
||||
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
||||
"LabelMarkSeries": "Označi seriju",
|
||||
"LabelMediaPlayer": "Media Player",
|
||||
@@ -229,6 +257,7 @@
|
||||
"LabelMissingParts": "Nedostajali dijelovi",
|
||||
"LabelMore": "Više",
|
||||
"LabelName": "Ime",
|
||||
"LabelNarrator": "Narrator",
|
||||
"LabelNarrators": "Naratori",
|
||||
"LabelNew": "Novo",
|
||||
"LabelNewestAuthors": "Najnoviji autori",
|
||||
@@ -236,15 +265,18 @@
|
||||
"LabelNewPassword": "Nova lozinka",
|
||||
"LabelNotes": "Bilješke",
|
||||
"LabelNotFinished": "Nedovršeno",
|
||||
"LabelNotificationEvent": "Notification Event",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
"LabelNotificationAvailableVariables": "Dostupne varijable",
|
||||
"LabelNotificationBodyTemplate": "Body Template",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotificationEvent": "Notification Event",
|
||||
"LabelNotificationsMaxFailedAttempts": "Maksimalan broj neuspjelih pokušaja",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Obavijesti će biti isključene ako par puta budu neuspješno poslane.",
|
||||
"LabelNotificationsMaxQueueSize": "Maksimalna veličina queuea za notification events",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Samo 1 event po sekundi može biti pokrenut. Eventi će biti ignorirani ako je queue na maksimalnoj veličini. To spriječava spammanje s obavijestima.",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotStarted": "Not Started",
|
||||
"LabelNumberOfBooks": "Number of Books",
|
||||
"LabelNumberOfEpisodes": "# of Episodes",
|
||||
"LabelOpenRSSFeed": "Otvori RSS Feed",
|
||||
"LabelPassword": "Lozinka",
|
||||
"LabelPath": "Putanja",
|
||||
@@ -256,6 +288,7 @@
|
||||
"LabelPermissionsUpdate": "Smije aktualizirati",
|
||||
"LabelPermissionsUpload": "Smije uploadati",
|
||||
"LabelPhotoPathURL": "Slika putanja/URL",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Vrsta reprodukcije",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
@@ -269,6 +302,8 @@
|
||||
"LabelRecentSeries": "Nedavne serije",
|
||||
"LabelRegion": "Regija",
|
||||
"LabelReleaseDate": "Datum izlaska",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||
"LabelRSSFeedURL": "RSS Feed URL",
|
||||
"LabelSearchTerm": "Traži pojam",
|
||||
@@ -278,6 +313,7 @@
|
||||
"LabelSequence": "Sekvenca",
|
||||
"LabelSeries": "Serije",
|
||||
"LabelSeriesName": "Ime serije",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorfski (što god to bilo) dizajn sa drvenim policama",
|
||||
"LabelSettingsChromecastSupport": "Chromecast podrška",
|
||||
"LabelSettingsDateFormat": "Format datuma",
|
||||
@@ -306,14 +342,15 @@
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "SPreskoči matchanje knjiga koje već imaju ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse tokom sortiranja",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "npr. za prefiks \"the\" book title \"The Ime Knjige\" će sortirati kao \"Ime Knjige, The\"",
|
||||
"LabelSettingsSquareBookCovers": "Kockasti cover knjige",
|
||||
"LabelSettingsSquareBookCoversHelp": "Koristi kockasti cover knjige umjesto klasičnog 1.6:1.",
|
||||
"LabelSettingsStoreCoversWithItem": "Spremi cover uz stakvu",
|
||||
"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",
|
||||
"LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.",
|
||||
"LabelSettingsSquareBookCovers": "Kockasti cover knjige",
|
||||
"LabelSettingsSquareBookCoversHelp": "Koristi kockasti cover knjige umjesto klasičnog 1.6:1.",
|
||||
"LabelShowAll": "Prikaži sve",
|
||||
"LabelSize": "Veličina",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelStart": "Pokreni",
|
||||
"LabelStarted": "Pokrenuto",
|
||||
"LabelStartedAt": "Pokrenuto",
|
||||
@@ -330,10 +367,12 @@
|
||||
"LabelStatsItemsInLibrary": "Stavke u biblioteki",
|
||||
"LabelStatsMinutes": "minute",
|
||||
"LabelStatsMinutesListening": "Minuta odslušano",
|
||||
"LabelStatsOverall": "Sveukupno",
|
||||
"LabelStatsOverallDays": "Overall Days",
|
||||
"LabelStatsOverallHours": "Overall Hours",
|
||||
"LabelStatsWeekListening": "Tjedno slušanje",
|
||||
"LabelSubtitle": "Podnapis",
|
||||
"LabelSupportedFileTypes": "Podržtani tip datoteke",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||
"LabelTimeListened": "Vremena odslušano",
|
||||
@@ -341,9 +380,19 @@
|
||||
"LabelTimeRemaining": "{0} preostalo",
|
||||
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
|
||||
"LabelTitle": "Naslov",
|
||||
"LabelToolsEmbedMetadata": "Embed Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
||||
"LabelTotalDuration": "Total Duration",
|
||||
"LabelTotalTimeListened": "Sveukupno vrijeme slušanja",
|
||||
"LabelTrackFromFilename": "Track iz imena datoteke",
|
||||
"LabelTrackFromMetadata": "Track iz metapodataka",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Tip",
|
||||
"LabelUnknown": "Nepoznato",
|
||||
"LabelUpdateCover": "Aktualiziraj Cover",
|
||||
@@ -359,23 +408,37 @@
|
||||
"LabelUsername": "Korisničko ime",
|
||||
"LabelValue": "Vrijednost",
|
||||
"LabelVersion": "Verzija",
|
||||
"LabelViewBookmarks": "View bookmarks",
|
||||
"LabelViewChapters": "View chapters",
|
||||
"LabelViewQueue": "View player queue",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Radnih dana da radi",
|
||||
"LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
|
||||
"LabelYourBookmarks": "Tvoje knjižne oznake",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Tvoj napredak",
|
||||
"MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz",
|
||||
"MessageBackupsNote": "Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match će probati dodati nedostale covere i metapodatke za odabrane stavke. Uključi postavke ispod da omočutie Quick Mathchu da zamijeni postojeće covere i/ili metapodatke.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"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.",
|
||||
"MessageCheckingCron": "Provjeravam cron...",
|
||||
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
|
||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
||||
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
||||
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
||||
"MessageEmbedFinished": "Embed završen!",
|
||||
@@ -408,6 +471,7 @@
|
||||
"MessageNoEpisodes": "Nema epizoda",
|
||||
"MessageNoFoldersAvailable": "Nema dostupnih foldera",
|
||||
"MessageNoGenres": "Nema žanrova",
|
||||
"MessageNoIssues": "No Issues",
|
||||
"MessageNoItems": "Nema stavki",
|
||||
"MessageNoItemsFound": "Nijedna stavka pronađena",
|
||||
"MessageNoListeningSessions": "Nema Listening Sessions",
|
||||
@@ -417,18 +481,28 @@
|
||||
"MessageNoPodcastsFound": "Nijedan podcast pronađen",
|
||||
"MessageNoResults": "Nema rezultata",
|
||||
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Not yet implemented",
|
||||
"MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
|
||||
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "or",
|
||||
"MessagePauseChapter": "Pause chapter playback",
|
||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||
"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.",
|
||||
"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?",
|
||||
"MessageRemoveChapter": "Remove chapter",
|
||||
"MessageRemoveEpisodes": "ukloni {0} epizoda/-e",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Jeste li sigurni da želite trajno obrisati korisnika \"{0}\"?",
|
||||
"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",
|
||||
"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",
|
||||
"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}?",
|
||||
"MessageThinking": "Razmišljam...",
|
||||
"MessageUploaderItemFailed": "Upload neuspješan",
|
||||
@@ -436,6 +510,7 @@
|
||||
"MessageUploading": "Uploadam...",
|
||||
"MessageValidCronExpression": "Ispravan cron expression",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher je globalno isključen u postavkama servera",
|
||||
"MessageXLibraryIsEmpty": "{0} Library is empty!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Trajanje audio knjige je duže nego pronadeđna duljina trajanja",
|
||||
"MessageYourAudiobookDurationIsShorter": "Trajanje audio knjige je kraća nego pronadeđna duljina trajanja",
|
||||
"NoteChangeRootPassword": "Root korisnik je jedini korisnik koji može imati praznu lozinku",
|
||||
@@ -449,6 +524,7 @@
|
||||
"NoteUploaderUnsupportedFiles": "Nepodržane datoteke su ignorirane. Kada birate ili ubacujete folder, ostale datoteke koje nisu folder će biti ignorirane.",
|
||||
"PlaceholderNewCollection": "Ime nove kolekcije",
|
||||
"PlaceholderNewFolderPath": "Nova folder putanja",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Traži...",
|
||||
"ToastAccountUpdateFailed": "Neuspješno aktualiziranje korisničkog računa",
|
||||
"ToastAccountUpdateSuccess": "Korisnički račun aktualiziran",
|
||||
@@ -473,6 +549,8 @@
|
||||
"ToastBookmarkRemoveSuccess": "Knjižnja bilješka uklonjena",
|
||||
"ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna",
|
||||
"ToastBookmarkUpdateSuccess": "Knjižna bilješka aktualizirana",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Neuspješno brisanje stavke/-i iz kolekcije",
|
||||
"ToastCollectionItemsRemoveSuccess": "Stavka/-e obrisane iz kolekcije",
|
||||
"ToastCollectionRemoveFailed": "Brisanje kolekcije neuspješno",
|
||||
@@ -496,14 +574,21 @@
|
||||
"ToastLibraryScanStarted": "Sken biblioteke pokrenut",
|
||||
"ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
|
||||
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Neuspješno kreiranje podcasta",
|
||||
"ToastPodcastCreateSuccess": "Podcast uspješno kreiran",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
||||
"ToastRemoveItemFromCollectionFailed": "Neuspješno uklanjanje stavke iz kolekcije",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
||||
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
|
||||
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
|
||||
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
||||
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
||||
"ToastSocketConnected": "Socket connected",
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
||||
"ToastUserDeleteSuccess": "Korisnik obrisan",
|
||||
"WeekdayFriday": "Petak",
|
||||
|
||||
@@ -4,34 +4,36 @@
|
||||
"ButtonAddPodcasts": "Aggiungi Podcast",
|
||||
"ButtonAddYourFirstLibrary": "Aggiungi la tua prima libreria",
|
||||
"ButtonApply": "Applica",
|
||||
"ButtonApplyChapters": "Apply Capitoli",
|
||||
"ButtonApplyChapters": "Applica",
|
||||
"ButtonAuthors": "Autori",
|
||||
"ButtonBrowseForFolder": "Per cartella",
|
||||
"ButtonBrowseForFolder": "Per Cartella",
|
||||
"ButtonCancel": "Cancella",
|
||||
"ButtonCancelEncode": "ferma la codifica",
|
||||
"ButtonCancelEncode": "Ferma la codifica",
|
||||
"ButtonChangeRootPassword": "Cambia la Password di root",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Controlla & scarica i nuovi episodi",
|
||||
"ButtonChooseAFolder": "Seleziona la Cartella",
|
||||
"ButtonChooseFiles": "Seleziona i File",
|
||||
"ButtonClearFilter": "Elimina Filtri",
|
||||
"ButtonCloseFeed": "Chudi i Feed",
|
||||
"ButtonCollections": "Collezioni",
|
||||
"ButtonCollections": "Raccolte",
|
||||
"ButtonConfigureScanner": "Configura Scanner",
|
||||
"ButtonCreate": "Crea",
|
||||
"ButtonCreateBackup": "Crea un Backup",
|
||||
"ButtonDelete": "Elimina",
|
||||
"ButtonEditChapters": "Modifica Capitoli",
|
||||
"ButtonEditPodcast": "Modifica Podcast",
|
||||
"ButtonForceReScan": "Forza Re-Scan",
|
||||
"ButtonFullPath": "percorso Completo",
|
||||
"ButtonFullPath": "Percorso Completo",
|
||||
"ButtonHide": "Nascondi",
|
||||
"ButtonHome": "Home",
|
||||
"ButtonIssues": "problemi",
|
||||
"ButtonIssues": "Errori",
|
||||
"ButtonLatest": "Ultimi",
|
||||
"ButtonLibrary": "Libreria",
|
||||
"ButtonLogout": "Disconnetti",
|
||||
"ButtonLookup": "Consulta",
|
||||
"ButtonLibrary": "Libreria",
|
||||
"ButtonManageTracks": "Gestisci le Tracce",
|
||||
"ButtonMapChapterTitles": "Titoli dei Capitoli",
|
||||
"ButtonMatchAllAuthors": "Aggiungi metadata degli autori",
|
||||
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
|
||||
"ButtonMatchBooks": "Aggiungi metadata della Libreria",
|
||||
"ButtonNevermind": "Nevermind",
|
||||
"ButtonOk": "Ok",
|
||||
@@ -39,12 +41,13 @@
|
||||
"ButtonOpenManager": "Apri Manager",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlaying": "In Riproduzione",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Elimina tutta la Cache",
|
||||
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
||||
"ButtonPurgeMediaProgress": "elimina info sui media ascoltati",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQuickMatch": "Ricerca meta Rapido",
|
||||
"ButtonPurgeMediaProgress": "Elimina info dei media ascoltati",
|
||||
"ButtonQueueAddItem": "Aggiungi alla Coda",
|
||||
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
||||
"ButtonQuickMatch": "Controlla Metadata Auto",
|
||||
"ButtonRead": "Leggi",
|
||||
"ButtonRemove": "Rimuovi",
|
||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||
@@ -57,11 +60,13 @@
|
||||
"ButtonSave": "Salva",
|
||||
"ButtonSaveAndClose": "Salva & Chiudi",
|
||||
"ButtonSaveTracklist": "Salva Tracklist",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScan": "Scansiona",
|
||||
"ButtonScanLibrary": "Scansiona Libreria",
|
||||
"ButtonSearch": "Cerca",
|
||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||
"ButtonSeries": "Serie",
|
||||
"ButtonShiftTimes": "Shift Times",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Ricerca veloce",
|
||||
"ButtonShow": "Mostra",
|
||||
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
||||
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
|
||||
@@ -74,22 +79,22 @@
|
||||
"ButtonYes": "Si",
|
||||
"HeaderAccount": "Account",
|
||||
"HeaderAdvanced": "Avanzate",
|
||||
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di notifica",
|
||||
"HeaderAudiobookTools": "utilità Audiobook File Management",
|
||||
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
|
||||
"HeaderAudiobookTools": "Utilità Audiobook File Management",
|
||||
"HeaderAudioTracks": "Tracce Audio",
|
||||
"HeaderBackups": "Backup",
|
||||
"HeaderChangePassword": "Cambia Password",
|
||||
"HeaderChapters": "Capitoli",
|
||||
"HeaderChooseAFolder": "Seleziona la cartella",
|
||||
"HeaderCollection": "Collezioni",
|
||||
"HeaderCollectionItems": "Elementi della Collezione",
|
||||
"HeaderCollection": "Raccolta",
|
||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderDetails": "Dettagli",
|
||||
"HeaderEpisodes": "Episodi",
|
||||
"HeaderFiles": "File",
|
||||
"HeaderFindChapters": "Trova Capitoli",
|
||||
"HeaderIgnoredFiles": "File Ignorati",
|
||||
"HeaderItemFiles": "Item Files",
|
||||
"HeaderItemFiles": "Files",
|
||||
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
||||
"HeaderLatestEpisodes": "Ultimi Episodi",
|
||||
"HeaderLibraries": "Librerie",
|
||||
@@ -99,22 +104,24 @@
|
||||
"HeaderListeningStats": "Statistiche di Ascolto",
|
||||
"HeaderLogin": "Login",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderMatch": "Match",
|
||||
"HeaderMatch": "Trova Corrispondenza",
|
||||
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
||||
"HeaderNewAccount": "Nuovo Account",
|
||||
"HeaderNewLibrary": "Nuova Libreria",
|
||||
"HeaderNotifications": "Notifiche",
|
||||
"HeaderOtherFiles": "Altri File",
|
||||
"HeaderOpenRSSFeed": "Apri RSS Feed",
|
||||
"HeaderOtherFiles": "Altri File",
|
||||
"HeaderPermissions": "Permessi",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlayerQueue": "Coda Riproduzione",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
||||
"HeaderPreviewCover": "Anteprima Cover",
|
||||
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
||||
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
||||
"HeaderSavedMediaProgress": "Progressi salvati",
|
||||
"HeaderSchedule": "schedula",
|
||||
"HeaderSchedule": "Schedula",
|
||||
"HeaderScheduleLibraryScans": "Schedula la scansione della libreria",
|
||||
"HeaderSession": "Sessione",
|
||||
"HeaderSetBackupSchedule": "Imposta programmazione Backup",
|
||||
@@ -129,7 +136,7 @@
|
||||
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autori",
|
||||
"HeaderStatsTop5Genres": "Top 5 Generi",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderTools": "Strumenti",
|
||||
"HeaderUpdateAccount": "Aggiorna Account",
|
||||
"HeaderUpdateAuthor": "Aggiorna Autore",
|
||||
"HeaderUpdateDetails": "Aggiorna Dettagli",
|
||||
@@ -141,43 +148,50 @@
|
||||
"LabelAccountTypeGuest": "Ospite",
|
||||
"LabelAccountTypeUser": "Utente",
|
||||
"LabelActivity": "Attività",
|
||||
"LabelAddToCollection": "Aggiungi alla Collezione",
|
||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Collezione",
|
||||
"LabelAddedAt": "Aggiunto il",
|
||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "All",
|
||||
"LabelAllUsers": "Tutti gli Utenti",
|
||||
"LabelAuthor": "Autore",
|
||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||
"LabelAuthors": "Autori",
|
||||
"LabelAutoDownloadEpisodes": "Auto Download Episodi",
|
||||
"LabelBackToUser": "Torna a Utenti",
|
||||
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "i Backup saranno salvati in /metadata/backups",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Dimensione massima backup (in GB)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Come protezione contro la configurazione errata, i backup falliranno se superano la dimensione configurata.",
|
||||
"LabelBackupsNumberToKeep": "Numbero di backup da mantenere",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Come protezione contro gli errori di config, i backup falliranno se superano la dimensione configurata.",
|
||||
"LabelBackupsNumberToKeep": "Numero di backup da mantenere",
|
||||
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
|
||||
"LabelBooks": "Libri",
|
||||
"LabelChangePassword": "Cambia Password",
|
||||
"LabelChaptersFound": "Capitoli Trovati",
|
||||
"LabelChapterTitle": "Titoli dei Capitoli",
|
||||
"LabelClosePlayer": "Chiudi player",
|
||||
"LabelCollapseSeries": "Comprimi Serie",
|
||||
"LabelCollections": "Collezioni",
|
||||
"LabelComplete": "Complete",
|
||||
"LabelCollections": "Raccolte",
|
||||
"LabelComplete": "Completo",
|
||||
"LabelConfirmPassword": "Conferma Password",
|
||||
"LabelContinueListening": "Continua ad Ascoltare",
|
||||
"LabelContinueSeries": "Continua Serie",
|
||||
"LabelCover": "Cover",
|
||||
"LabelCoverImageURL": "Cover Image URL",
|
||||
"LabelCreatedAt": "Creato A",
|
||||
"LabelCronExpression": "Cron Expression",
|
||||
"LabelCronExpression": "Espressione Cron",
|
||||
"LabelCurrent": "Attuale",
|
||||
"LabelCurrently": "Attualmente:",
|
||||
"LabelDatetime": "Datetime",
|
||||
"LabelDatetime": "Data & Ora",
|
||||
"LabelDescription": "Descrizione",
|
||||
"LabelDeselectAll": "Deseleziona Tutto",
|
||||
"LabelDevice": "Device",
|
||||
"LabelDeviceInfo": "Device Info",
|
||||
"LabelDevice": "Dispositivo",
|
||||
"LabelDeviceInfo": "Info Dispositivo",
|
||||
"LabelDirectory": "Elenco",
|
||||
"LabelDiscFromFilename": "Disc from Filename",
|
||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||
"LabelDiscFromFilename": "Disco dal nome file",
|
||||
"LabelDiscFromMetadata": "Disco dal Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDuration": "Durata",
|
||||
"LabelDurationFound": "Durata Trovata:",
|
||||
@@ -190,12 +204,15 @@
|
||||
"LabelExplicit": "Esplicito",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "File",
|
||||
"LabelFileBirthtime": "Data Creazione",
|
||||
"LabelFileModified": "Ultima modifica",
|
||||
"LabelFilename": "Nome File",
|
||||
"LabelFilterByUser": "Filter per Utente",
|
||||
"LabelFilterByUser": "Filtro per Utente",
|
||||
"LabelFindEpisodes": "Trova Episodi",
|
||||
"LabelFinished": "Finita",
|
||||
"LabelFolder": "Cartella",
|
||||
"LabelFolders": "cartelle",
|
||||
"LabelFolders": "Cartelle",
|
||||
"LabelGenre": "Genere",
|
||||
"LabelGenres": "Generi",
|
||||
"LabelHardDeleteFile": "Elimina Definitivamente",
|
||||
"LabelHour": "Ora",
|
||||
@@ -204,6 +221,14 @@
|
||||
"LabelIncomplete": "Incompleta",
|
||||
"LabelInProgress": "In Corso",
|
||||
"LabelInterval": "Intervallo",
|
||||
"LabelIntervalCustomDailyWeekly": "Personalizza giorni/settimane",
|
||||
"LabelIntervalEvery12Hours": "EOgni 12 Ore",
|
||||
"LabelIntervalEvery15Minutes": "Ogni 15 Minuti",
|
||||
"LabelIntervalEvery2Hours": "Ogni 2 Ore",
|
||||
"LabelIntervalEvery30Minutes": "Ogni 30 Minuti",
|
||||
"LabelIntervalEvery6Hours": "Ogni 6 ore",
|
||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||
"LabelIntervalEveryHour": "Ogni ora",
|
||||
"LabelInvalidParts": "Parti Invalide",
|
||||
"LabelItem": "Oggetti",
|
||||
"LabelLanguage": "Lingua",
|
||||
@@ -218,6 +243,9 @@
|
||||
"LabelLibraryName": "Nome Libreria",
|
||||
"LabelLimit": "Limiti",
|
||||
"LabelListenAgain": "Ri-ascolta",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Allarme",
|
||||
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
||||
"LabelMarkSeries": "Segna Serie",
|
||||
"LabelMediaPlayer": "Media Player",
|
||||
@@ -225,26 +253,30 @@
|
||||
"LabelMetadataProvider": "Metadata Provider",
|
||||
"LabelMetaTag": "Meta Tag",
|
||||
"LabelMinute": "Minuto",
|
||||
"LabelMissing": "Rimanente",
|
||||
"LabelMissing": "Altro",
|
||||
"LabelMissingParts": "Parti rimantenti",
|
||||
"LabelMore": "Espandi",
|
||||
"LabelName": "Nome",
|
||||
"LabelNarrators": "Narratore",
|
||||
"LabelNarrator": "Narratore",
|
||||
"LabelNarrators": "Narratori",
|
||||
"LabelNew": "Nuovo",
|
||||
"LabelNewestAuthors": "Autori Recenti",
|
||||
"LabelNewestEpisodes": "Episodi Recenti",
|
||||
"LabelNewPassword": "Nuova Password",
|
||||
"LabelNotes": "Note",
|
||||
"LabelNotFinished": "Non Finita",
|
||||
"LabelNotificationEvent": "Notifiche Eventi",
|
||||
"LabelNotFinished": "Da Completare",
|
||||
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
||||
"LabelNotificationAvailableVariables": "Variabili Selezionabili",
|
||||
"LabelNotificationBodyTemplate": "Body Template",
|
||||
"LabelNotificationTitleTemplate": "Title Template",
|
||||
"LabelNotificationBodyTemplate": "Template del corpo messaggio",
|
||||
"LabelNotificationEvent": "Notifiche Eventi",
|
||||
"LabelNotificationsMaxFailedAttempts": "Numero massimo di tentativi falliti",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Le notifiche vengono disabilitate se falliscono molte volte",
|
||||
"LabelNotificationsMaxQueueSize": "Coda Massima di notifiche eventi",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Le notifiche sono limitate per 1 al secondo, per evitare lo spamming le notifiche verrano ignorare se superano la coda",
|
||||
"LabelNotificationTitleTemplate": "Template del titolo",
|
||||
"LabelNotStarted": "Non iniziato",
|
||||
"LabelNumberOfBooks": "Numero di libri",
|
||||
"LabelNumberOfEpisodes": "# degli episodi",
|
||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||
"LabelPassword": "Password",
|
||||
"LabelPath": "Percorso",
|
||||
@@ -254,13 +286,14 @@
|
||||
"LabelPermissionsDelete": "Può Cancellare",
|
||||
"LabelPermissionsDownload": "Può Scaricare",
|
||||
"LabelPermissionsUpdate": "Può Aggiornare",
|
||||
"LabelPermissionsUpload": "Can caricare",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPermissionsUpload": "Può caricare",
|
||||
"LabelPhotoPathURL": "foto Path/URL",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Metodo di riproduzione",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
"LabelPrefixesToIgnore": "suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||
"LabelProgress": "Progresso",
|
||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||
"LabelProgress": "Cominciati",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Data Pubblicazione",
|
||||
"LabelPublisher": "Editore",
|
||||
@@ -268,16 +301,19 @@
|
||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||
"LabelRecentSeries": "Serie Recenti",
|
||||
"LabelRegion": "Regione",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelReleaseDate": "Data Release",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRSSFeedOpen": "RSS Feed Aperto",
|
||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||
"LabelRSSFeedURL": "RSS Feed URL",
|
||||
"LabelSearchTerm": "Termini di Ricerca",
|
||||
"LabelSearchTerm": "Ricerca",
|
||||
"LabelSearchTitle": "Cerca Titolo",
|
||||
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
||||
"LabelSeason": "Stagione",
|
||||
"LabelSequence": "Sequenza",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Nome Serie",
|
||||
"LabelSeriesProgress": "Cominciato",
|
||||
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
||||
"LabelSettingsChromecastSupport": "Supporto a Chromecast",
|
||||
"LabelSettingsDateFormat": "Formato Data",
|
||||
@@ -294,56 +330,69 @@
|
||||
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
|
||||
"LabelSettingsOverdriveMediaMarkersHelp": "I file MP3 di Overdrive vengono forniti con i tempi dei capitoli incorporati come metadati personalizzati. Abilitando questa funzione verranno utilizzati automaticamente questi tag per i tempi dei capitoli",
|
||||
"LabelSettingsParseSubtitles": "Analizza subtitles",
|
||||
"LabelSettingsParseSubtitles": "Analizza sottotitoli",
|
||||
"LabelSettingsParseSubtitlesHelp": "Estrai i sottotitoli dai nomi delle cartelle degli audiolibri. <br> I sottotitoli devono essere separati da \" - \"<br> Per esempio \"Il signore degli anelli - Le due Torri \" avrà il sottotitolo \"Le due Torri\"",
|
||||
"LabelSettingsPreferAudioMetadata": "Preferisci i metadati audio",
|
||||
"LabelSettingsPreferAudioMetadataHelp": "I meta tag ID3 del file audio verrano preferiti rispetto al nome della cartella",
|
||||
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
|
||||
"LabelSettingsPreferMatchedMetadata": "Preferisci i metadata trovati",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "I dati trovati in internet sovrascriveranno i dettagli del libro quando si utilizza quick Match. Per impostazione predefinita, Quick Match riempirà solo i dettagli mancanti.",
|
||||
"LabelSettingsPreferOPFMetadata": "Preferisci OPF metadata",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "I metadati del file OPF verranno utilizzati per i dettagli del libro e non il nome della cartella",
|
||||
"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",
|
||||
"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\" cone nel libro \"Il signore degli anelli\" che verrebbe ordinato come \"signore degli anelli, il\"",
|
||||
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
||||
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
|
||||
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs",
|
||||
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
||||
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
||||
"LabelShowAll": "Mostra Tutto",
|
||||
"LabelSize": "Dimensione",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelStart": "Inizo",
|
||||
"LabelStarted": "Iniziato",
|
||||
"LabelStartedAt": "Iniziato al",
|
||||
"LabelStartTime": "Start Time",
|
||||
"LabelStartTime": "Tempo di inizio",
|
||||
"LabelStatsAudioTracks": "Tracce Audio",
|
||||
"LabelStatsAuthors": "Autori",
|
||||
"LabelStatsBestDay": "Best Day",
|
||||
"LabelStatsBestDay": "Giorno Migliore",
|
||||
"LabelStatsDailyAverage": "Media giornaliera",
|
||||
"LabelStatsDays": "Giorni",
|
||||
"LabelStatsDaysListened": "Giorni di Ascolto",
|
||||
"LabelStatsHours": "ore",
|
||||
"LabelStatsHours": "Ore",
|
||||
"LabelStatsInARow": "Di fila",
|
||||
"LabelStatsItemsFinished": "Libri Completati",
|
||||
"LabelStatsItemsInLibrary": "Libri in Libreria",
|
||||
"LabelStatsMinutes": "minuti",
|
||||
"LabelStatsMinutes": "Minuti",
|
||||
"LabelStatsMinutesListening": "Ascolto in Minuti",
|
||||
"LabelStatsOverall": "Complessivamente",
|
||||
"LabelStatsOverallDays": "Giorni Complessivi",
|
||||
"LabelStatsOverallHours": "Ore Complessive",
|
||||
"LabelStatsWeekListening": "Ascolto Settimanale",
|
||||
"LabelSubtitle": "Sottotitoli",
|
||||
"LabelSupportedFileTypes": "Tipi di file Supportati",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||
"LabelTimeListened": "Tempo di Ascolto",
|
||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||
"LabelTimeRemaining": "{0} rimanente",
|
||||
"LabelTimeToShift": "Time to shift in seconds",
|
||||
"LabelTimeToShift": "Tempo di shift in secondi",
|
||||
"LabelTitle": "Titolo",
|
||||
"LabelToolsEmbedMetadata": "Incorpora Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
|
||||
"LabelToolsMakeM4b": "Crea un file M4B",
|
||||
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
|
||||
"LabelToolsSplitM4b": "Converti M4B in MP3's",
|
||||
"LabelToolsSplitM4bDescription": "Crea MP3 da un M4B diviso per capitoli con metadati incorporati, immagine di copertina e capitoli.",
|
||||
"LabelTotalDuration": "Durata Totale",
|
||||
"LabelTotalTimeListened": "Tempo totale di Ascolto",
|
||||
"LabelTrackFromFilename": "Traccia da nome file",
|
||||
"LabelTrackFromMetadata": "Traccia da Metadata",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Tipo",
|
||||
"LabelUnknown": "Sconosciuto",
|
||||
"LabelUpdateCover": "Aggiornamento Cover",
|
||||
@@ -354,28 +403,42 @@
|
||||
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
||||
"LabelUploaderDropFiles": "Elimina file",
|
||||
"LabelUseChapterTrack": "Usa il Capitolo della Traccia",
|
||||
"LabelUseFullTrack": "Usa la full track",
|
||||
"LabelUseFullTrack": "Usa la traccia totale",
|
||||
"LabelUser": "Utente",
|
||||
"LabelUsername": "Username",
|
||||
"LabelValue": "Valore",
|
||||
"LabelVersion": "Versione",
|
||||
"LabelViewBookmarks": "Visualizza i Segnalibri",
|
||||
"LabelViewChapters": "Visualizza i Capitoli",
|
||||
"LabelViewQueue": "Visualizza coda",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||
"LabelYourAudiobookDuration": "la tua durata dell'audiolibri",
|
||||
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
||||
"LabelYourBookmarks": "I tuoi Preferiti",
|
||||
"LabelYourProgress": "I tuoi Progressi",
|
||||
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in",
|
||||
"MessageBackupsNote": "I backup non includono i file archiviati nelle cartelle della libreria.",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Completato al",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"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.",
|
||||
"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 ",
|
||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||
"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": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageCheckingCron": "Controllo cron...",
|
||||
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Collezioni \"{0}\"?",
|
||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||
"MessageEmbedFinished": "Incorporamento finito!",
|
||||
@@ -391,7 +454,7 @@
|
||||
"MessageLoading": "Caricamento...",
|
||||
"MessageLoadingFolders": "Caricamento Cartelle...",
|
||||
"MessageM4BFailed": "M4B Fallito!",
|
||||
"MessageM4BFinished": "M4B Fiinito!",
|
||||
"MessageM4BFinished": "M4B Finito!",
|
||||
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
|
||||
"MessageMarkAsFinished": "Segna come finito",
|
||||
"MessageMarkAsNotFinished": "Segna come da completare",
|
||||
@@ -401,13 +464,14 @@
|
||||
"MessageNoBackups": "Nessun Backup",
|
||||
"MessageNoBookmarks": "Nessun Preferito",
|
||||
"MessageNoChapters": "Nessun Capitolo",
|
||||
"MessageNoCollections": "Nessuna Collezione",
|
||||
"MessageNoCollections": "Nessuna Raccolta",
|
||||
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
||||
"MessageNoDescription": "Nessuna descrizione",
|
||||
"MessageNoEpisodeMatchesFound": "Nessun episodio corrispondente trovato",
|
||||
"MessageNoEpisodes": "Nessun Episodio",
|
||||
"MessageNoFoldersAvailable": "Nessuna Cartella disponibile",
|
||||
"MessageNoGenres": "Nessun Genere",
|
||||
"MessageNoIssues": "Nessun Errore",
|
||||
"MessageNoItems": "Nessun Oggetto",
|
||||
"MessageNoItemsFound": "Nessun Oggetto trovato",
|
||||
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
||||
@@ -417,25 +481,36 @@
|
||||
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
||||
"MessageNoResults": "Nessun Risultato",
|
||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "o",
|
||||
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
||||
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
||||
"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\".",
|
||||
"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",
|
||||
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Sei sicuro di voler eliminare definitivamente l'utente \"{0}\"?",
|
||||
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||
"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.",
|
||||
"MessageSearchResultsFor": "cerca risultati per",
|
||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
||||
"MessageThinking": "Thinking...",
|
||||
"MessageThinking": "Elaborazione...",
|
||||
"MessageUploaderItemFailed": "Caricamento Fallito",
|
||||
"MessageUploaderItemSuccess": "Caricato con successo!",
|
||||
"MessageUploading": "Caricamento...",
|
||||
"MessageValidCronExpression": "Valid cron expression",
|
||||
"MessageValidCronExpression": "Espressione Cron Valida",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher è disabilitato a livello globale nelle impostazioni del server",
|
||||
"MessageXLibraryIsEmpty": "{0} libreria vuota!",
|
||||
"MessageYourAudiobookDurationIsLonger": "La durata dell'audiolibro è più lunga della durata trovata",
|
||||
"MessageYourAudiobookDurationIsShorter": "La durata dell'audiolibro è inferiore alla durata trovata",
|
||||
"NoteChangeRootPassword": "L'utente root è l'unico utente che può avere una password vuota",
|
||||
@@ -447,8 +522,9 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Le cartelle con file multimediali verranno gestite come elementi della libreria separati.",
|
||||
"NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.",
|
||||
"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 Collezione",
|
||||
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
||||
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Cerca..",
|
||||
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
||||
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
||||
@@ -460,27 +536,29 @@
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Autore aggiornato (nessuna immagine trovata)",
|
||||
"ToastBackupCreateFailed": "creazione backup fallita",
|
||||
"ToastBackupCreateSuccess": "Backup creato",
|
||||
"ToastBackupDeleteFailed": "eliminazione backup fallita",
|
||||
"ToastBackupDeleteFailed": "Eliminazione backup fallita",
|
||||
"ToastBackupDeleteSuccess": "backup Eliminato",
|
||||
"ToastBackupRestoreFailed": "Ripristino fallito",
|
||||
"ToastBackupUploadFailed": "Caricamento backup fallito",
|
||||
"ToastBackupUploadSuccess": "Backup caricato",
|
||||
"ToastBatchUpdateFailed": "Batch di aggiornamento fallito",
|
||||
"ToastBatchUpdateSuccess": "Batch di aggiornamento finito",
|
||||
"ToastBookmarkCreateFailed": "creazione segnalibro fallita",
|
||||
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
|
||||
"ToastBookmarkCreateSuccess": "Segnalibro creato",
|
||||
"ToastBookmarkRemoveFailed": "Rimozione Segnalibro fallita",
|
||||
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
||||
"ToastBookmarkUpdateFailed": "Aggiornamento Segnalibro fallito",
|
||||
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
||||
"ToastCollectionItemsRemoveFailed": "rimozione oggetti dalla collezione fallita",
|
||||
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla collezione",
|
||||
"ToastCollectionRemoveFailed": "rimozione collezione fallita",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
|
||||
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
|
||||
"ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
|
||||
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
||||
"ToastCollectionUpdateFailed": "Errore aggiornamento collezione",
|
||||
"ToastCollectionUpdateSuccess": "Collezione aggiornata",
|
||||
"ToastCollectionUpdateFailed": "Errore aggiornamento Raccolta",
|
||||
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
||||
"ToastItemCoverUpdateFailed": "Errore Aggiornamento cover",
|
||||
"ToastItemCoverUpdateSuccess": "cover aggiornata",
|
||||
"ToastItemCoverUpdateSuccess": "Cover aggiornata",
|
||||
"ToastItemDetailsUpdateFailed": "Errore Aggiornamento dettagli file",
|
||||
"ToastItemDetailsUpdateSuccess": "Dettagli file Aggiornata",
|
||||
"ToastItemDetailsUpdateUnneeded": "Nessun Aggiornamento necessario per il file",
|
||||
@@ -496,15 +574,22 @@
|
||||
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
||||
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
||||
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla collezione",
|
||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla collezione",
|
||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||
"ToastUserDeleteFailed": "errore eliminazione utente",
|
||||
"ToastSocketConnected": "Socket connesso",
|
||||
"ToastSocketDisconnected": "Socket disconnesso",
|
||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||
"ToastUserDeleteSuccess": "Utente eliminato",
|
||||
"WeekdayFriday": "Venerdì",
|
||||
"WeekdayMonday": "Lunedì",
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Sprawdź i pobierz nowe odcinki",
|
||||
"ButtonChooseAFolder": "Wybierz folder",
|
||||
"ButtonChooseFiles": "Wybierz pliki",
|
||||
"ButtonClearFilter": "Wyczyść filtr",
|
||||
"ButtonCloseFeed": "Zamknij kanał",
|
||||
"ButtonCollections": "Kolekcje",
|
||||
"ButtonConfigureScanner": "Configure Scanner",
|
||||
"ButtonCreate": "Utwórz",
|
||||
"ButtonCreateBackup": "Utwórz kopię zapasową",
|
||||
"ButtonDelete": "Usuń",
|
||||
@@ -26,9 +28,9 @@
|
||||
"ButtonHome": "Strona główna",
|
||||
"ButtonIssues": "Błędy",
|
||||
"ButtonLatest": "Aktualna wersja:",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonLogout": "Wyloguj",
|
||||
"ButtonLookup": "Importuj",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonManageTracks": "Zarządzaj ścieżkami",
|
||||
"ButtonMapChapterTitles": "Mapuj nazwy rozdziałów",
|
||||
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
|
||||
@@ -39,11 +41,12 @@
|
||||
"ButtonOpenManager": "Otwórz menadżera",
|
||||
"ButtonPlay": "Odtwarzaj",
|
||||
"ButtonPlaying": "Odtwarzane",
|
||||
"ButtonPlaylists": "Playlists",
|
||||
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
|
||||
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
|
||||
"ButtonPurgeMediaProgress": "Wyczyść postęp",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQueueAddItem": "Dodaj do kolejki",
|
||||
"ButtonQueueRemoveItem": "Usuń z kolejki",
|
||||
"ButtonQuickMatch": "Szybkie dopasowanie",
|
||||
"ButtonRead": "Czytaj",
|
||||
"ButtonRemove": "Usuń",
|
||||
@@ -58,14 +61,16 @@
|
||||
"ButtonSaveAndClose": "Zapisz i zamknij",
|
||||
"ButtonSaveTracklist": "Zapisz listę odtwarzania",
|
||||
"ButtonScan": "Zeskanuj",
|
||||
"ButtonScanLibrary": "Scan Library",
|
||||
"ButtonSearch": "Szukaj",
|
||||
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
|
||||
"ButtonSeries": "Seria",
|
||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
||||
"ButtonShiftTimes": "Przesunięcie czasowe",
|
||||
"ButtonShow": "Pokaż",
|
||||
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
||||
"ButtonStartMetadataEmbed": "Osadź metadane",
|
||||
"ButtonSubmit": "Zgłoś",
|
||||
"ButtonSubmit": "Zaloguj",
|
||||
"ButtonUpload": "Wgraj",
|
||||
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
||||
"ButtonUploadCover": "Wgraj okładkę",
|
||||
@@ -93,7 +98,7 @@
|
||||
"HeaderLastListeningSession": "Ostatnio odtwarzana sesja",
|
||||
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
||||
"HeaderLibraries": "Biblioteki",
|
||||
"HeaderLibraryFiles": "Library Files",
|
||||
"HeaderLibraryFiles": "Pliki w bibliotece",
|
||||
"HeaderLibraryStats": "Statystyki biblioteki",
|
||||
"HeaderListeningSessions": "Sesje słuchania",
|
||||
"HeaderListeningStats": "Statystyki odtwarzania",
|
||||
@@ -104,10 +109,12 @@
|
||||
"HeaderNewAccount": "Nowe konto",
|
||||
"HeaderNewLibrary": "Nowa biblioteka",
|
||||
"HeaderNotifications": "Powiadomienia",
|
||||
"HeaderOtherFiles": "Inne pliki",
|
||||
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
||||
"HeaderOtherFiles": "Inne pliki",
|
||||
"HeaderPermissions": "Uprawnienia",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Playlist Items",
|
||||
"HeaderPodcastsToAdd": "Podcasty do dodania",
|
||||
"HeaderPreviewCover": "Podgląd okładki",
|
||||
"HeaderRemoveEpisode": "Usuń odcinek",
|
||||
@@ -141,10 +148,16 @@
|
||||
"LabelAccountTypeGuest": "Gość",
|
||||
"LabelAccountTypeUser": "Użytkownik",
|
||||
"LabelActivity": "Aktywność",
|
||||
"LabelAddedAt": "Dodano",
|
||||
"LabelAddToCollection": "Dodaj do kolekcji",
|
||||
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
|
||||
"LabelAddToPlaylist": "Add to Playlist",
|
||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||
"LabelAll": "All",
|
||||
"LabelAllUsers": "Wszyscy użytkownicy",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
||||
"LabelAuthorLastFirst": "Author (Malejąco)",
|
||||
"LabelAuthors": "Autorzy",
|
||||
"LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków",
|
||||
"LabelBackToUser": "Powrót",
|
||||
@@ -158,6 +171,7 @@
|
||||
"LabelChangePassword": "Zmień hasło",
|
||||
"LabelChaptersFound": "Znalezione rozdziały",
|
||||
"LabelChapterTitle": "Tytuł rozdziału",
|
||||
"LabelClosePlayer": "Zamknij odtwarzacz",
|
||||
"LabelCollapseSeries": "Podsumuj serię",
|
||||
"LabelCollections": "Kolekcje",
|
||||
"LabelComplete": "Ukończone",
|
||||
@@ -190,12 +204,15 @@
|
||||
"LabelExplicit": "Nieprzyzwoite",
|
||||
"LabelFeedURL": "URL kanału",
|
||||
"LabelFile": "Plik",
|
||||
"LabelFileBirthtime": "Data utworzenia pliku",
|
||||
"LabelFileModified": "Data modyfikacji pliku",
|
||||
"LabelFilename": "Nazwa pliku",
|
||||
"LabelFilterByUser": "Filtruj według danego użytkownika",
|
||||
"LabelFindEpisodes": "Znajdź odcinki",
|
||||
"LabelFinished": "Zakończone",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Foldery",
|
||||
"LabelGenre": "Gatunek",
|
||||
"LabelGenres": "Gatunki",
|
||||
"LabelHardDeleteFile": "Usuń trwale plik",
|
||||
"LabelHour": "Godzina",
|
||||
@@ -204,6 +221,14 @@
|
||||
"LabelIncomplete": "Nieukończone",
|
||||
"LabelInProgress": "W trakcie",
|
||||
"LabelInterval": "Interwał",
|
||||
"LabelIntervalCustomDailyWeekly": "Niestandardowy dzienny/tygodniowy",
|
||||
"LabelIntervalEvery12Hours": "Co 12 godzin",
|
||||
"LabelIntervalEvery15Minutes": "Co 15 minut",
|
||||
"LabelIntervalEvery2Hours": "Co 2 godziny",
|
||||
"LabelIntervalEvery30Minutes": "Co 30 minut",
|
||||
"LabelIntervalEvery6Hours": "Co 6 godzin",
|
||||
"LabelIntervalEveryDay": "Każdego dnia",
|
||||
"LabelIntervalEveryHour": "Każdej godziny",
|
||||
"LabelInvalidParts": "Nieprawidłowe części",
|
||||
"LabelItem": "Pozycja",
|
||||
"LabelLanguage": "Język",
|
||||
@@ -218,6 +243,9 @@
|
||||
"LabelLibraryName": "Nazwa biblioteki",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelListenAgain": "Słuchaj ponownie",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Informacja",
|
||||
"LabelLogLevelWarn": "Ostrzeżenie",
|
||||
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
||||
"LabelMarkSeries": "Oznacz serię",
|
||||
"LabelMediaPlayer": "Odtwarzacz",
|
||||
@@ -229,6 +257,7 @@
|
||||
"LabelMissingParts": "Brakujące cześci",
|
||||
"LabelMore": "Więcej",
|
||||
"LabelName": "Nazwa",
|
||||
"LabelNarrator": "Narrator",
|
||||
"LabelNarrators": "Lektorzy",
|
||||
"LabelNew": "Nowy",
|
||||
"LabelNewestAuthors": "Najnowsi autorzy",
|
||||
@@ -236,15 +265,18 @@
|
||||
"LabelNewPassword": "Nowe hasło",
|
||||
"LabelNotes": "Uwagi",
|
||||
"LabelNotFinished": "Nieukończone",
|
||||
"LabelNotificationEvent": "Zdarzenie",
|
||||
"LabelNotificationAppriseURL": "URLe Apprise",
|
||||
"LabelNotificationAvailableVariables": "Dostępne zmienne",
|
||||
"LabelNotificationBodyTemplate": "Szablon treści powiadomienia",
|
||||
"LabelNotificationTitleTemplate": "Szablon tytułu powiadmienia",
|
||||
"LabelNotificationEvent": "Zdarzenie",
|
||||
"LabelNotificationsMaxFailedAttempts": "Maksymalna liczba nieudanych prób",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Powiadomienia są wyłączane, gdy próba ich wysyłki nie powiedzie się kilkukrotnie",
|
||||
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
|
||||
"LabelNotificationTitleTemplate": "Szablon tytułu powiadmienia",
|
||||
"LabelNotStarted": "Nie rozpoęczto",
|
||||
"LabelNumberOfBooks": "Liczba książek",
|
||||
"LabelNumberOfEpisodes": "# odcinków",
|
||||
"LabelOpenRSSFeed": "Otwórz kanał RSS",
|
||||
"LabelPassword": "Hasło",
|
||||
"LabelPath": "Ścieżka",
|
||||
@@ -256,6 +288,7 @@
|
||||
"LabelPermissionsUpdate": "Ma możliwość aktualizowania",
|
||||
"LabelPermissionsUpload": "Ma możliwość dodawania",
|
||||
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Metoda odtwarzania",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasty",
|
||||
@@ -269,6 +302,8 @@
|
||||
"LabelRecentSeries": "Ostatnie serie",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Data wydania",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRSSFeedOpen": "RSS Feed otwarty",
|
||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||
"LabelRSSFeedURL": "URL kanały RSS",
|
||||
"LabelSearchTerm": "Wyszukiwanie frazy",
|
||||
@@ -278,6 +313,7 @@
|
||||
"LabelSequence": "Kolejność",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Nazwy serii",
|
||||
"LabelSeriesProgress": "Postęp w serii",
|
||||
"LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami",
|
||||
"LabelSettingsChromecastSupport": "Wsparcie Chromecast",
|
||||
"LabelSettingsDateFormat": "Format daty",
|
||||
@@ -306,14 +342,15 @@
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Pomiń dopasowanie książek, które już mają ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignoruj prefiksy podczas sortowania",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "np. dla prefiksu \"the\" tytuł ksiązki \"The Book Title\" będzie sortowany jako \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "Używaj kwadratowych okładek książek",
|
||||
"LabelSettingsSquareBookCoversHelp": "Preferuj stosowanie kwadratowych okładek zamiast standardowych okładek książkowych o propocji 1,6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Przechowuj okładkę w folderze książki",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs",
|
||||
"LabelSettingsSquareBookCovers": "Używaj kwadratowych okładek książek",
|
||||
"LabelSettingsSquareBookCoversHelp": "Preferuj stosowanie kwadratowych okładek zamiast standardowych okładek książkowych o propocji 1,6:1",
|
||||
"LabelShowAll": "Pokaż wszystko",
|
||||
"LabelSize": "Rozmiar",
|
||||
"LabelSleepTimer": "Wyłącznik czasowy",
|
||||
"LabelStart": "Rozpocznij",
|
||||
"LabelStarted": "Rozpoczęty",
|
||||
"LabelStartedAt": "Rozpoczęto",
|
||||
@@ -330,10 +367,12 @@
|
||||
"LabelStatsItemsInLibrary": "Pozycje w bibliotece",
|
||||
"LabelStatsMinutes": "Minuty",
|
||||
"LabelStatsMinutesListening": "Minuty odtwarzania",
|
||||
"LabelStatsOverall": "Ogólnie",
|
||||
"LabelStatsOverallDays": "Całkowity czas (dni)",
|
||||
"LabelStatsOverallHours": "Całkowity czas (godziny)",
|
||||
"LabelStatsWeekListening": "Tydzień odtwarzania",
|
||||
"LabelSubtitle": "Podtytuł",
|
||||
"LabelSupportedFileTypes": "Obsługiwane typy plików",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tagi",
|
||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||
"LabelTimeListened": "Czas odtwarzania",
|
||||
@@ -341,41 +380,65 @@
|
||||
"LabelTimeRemaining": "Pozostało {0}",
|
||||
"LabelTimeToShift": "Czas do przesunięcia w sekundach",
|
||||
"LabelTitle": "Tytuł",
|
||||
"LabelToolsEmbedMetadata": "Załącz metadane",
|
||||
"LabelToolsEmbedMetadataDescription": "Załącz metadane do plików audio (okładkę oraz znaczniki rozdziałów)",
|
||||
"LabelToolsMakeM4b": "Generuj plik M4B",
|
||||
"LabelToolsMakeM4bDescription": "Tworzy plik w formacie .M4B, który zawiera metadane, okładkę oraz rozdziały.",
|
||||
"LabelToolsSplitM4b": "Podziel plik .M4B na pliki .MP3",
|
||||
"LabelToolsSplitM4bDescription": "Podziel plik .M4B na pliki .MP3 na rozdziały z załączonymi metadanymi oraz okładką.",
|
||||
"LabelTotalDuration": "TCałkowita długość",
|
||||
"LabelTotalTimeListened": "Całkowity czas odtwarzania",
|
||||
"LabelTrackFromFilename": "Ścieżka z nazwy pliku",
|
||||
"LabelTrackFromMetadata": "Ścieżka z metadanych",
|
||||
"LabelTracks": "Tracks",
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelType": "Typ",
|
||||
"LabelUnknown": "Nieznany",
|
||||
"LabelUpdateCover": "Zaktalizuj odkładkę",
|
||||
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
|
||||
"LabelUpdatedAt": "Zaktualizaowano",
|
||||
"LabelUpdatedAt": "Zaktualizowano",
|
||||
"LabelUpdateDetails": "Zaktualizuj szczegóły",
|
||||
"LabelUpdateDetailsHelp": "Umożliwienie nadpisania istniejących szczegółów dla wybranych książek w przypadku znalezienia dopasowania",
|
||||
"LabelUploaderDragAndDrop": "Przeciągnij i puść foldery lub pliki",
|
||||
"LabelUploaderDropFiles": "Puść pliki",
|
||||
"LabelUseChapterTrack": "Use chapter track",
|
||||
"LabelUseChapterTrack": "Użyj ścieżki rozdziału",
|
||||
"LabelUseFullTrack": "Użycie ścieżki rozdziału",
|
||||
"LabelUser": "Użytkownik",
|
||||
"LabelUsername": "Nazwa użytkownika",
|
||||
"LabelValue": "Wartość",
|
||||
"LabelVersion": "Wersja",
|
||||
"Dni tygodnia": "Weekdays to run",
|
||||
"LabelViewBookmarks": "Wyświetlaj zakładki",
|
||||
"LabelViewChapters": "Wyświetlaj rozdziały",
|
||||
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
|
||||
"LabelVolume": "Głośność",
|
||||
"LabelWeekdaysToRun": "Dni tygodnia",
|
||||
"LabelYourAudiobookDuration": "Czas trwania audiobooka",
|
||||
"LabelYourBookmarks": "Twoje zakładki",
|
||||
"LabelYourPlaylists": "Your Playlists",
|
||||
"LabelYourProgress": "Twój postęp",
|
||||
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w",
|
||||
"MessageBackupsNote": "Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
|
||||
"MessageAddToPlayerQueue": "Add to player queue",
|
||||
"MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w <code>/metadata/items</code> & <code>/metadata/authors</code>. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.",
|
||||
"MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji",
|
||||
"MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Nie posiadasz żadnych otwartych feedów RSS",
|
||||
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
|
||||
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
|
||||
"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": "Początek rozdziału następuje po zakończeniu audiobooka",
|
||||
"MessageCheckingCron": "Sprawdzanie cron...",
|
||||
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
|
||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
||||
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
||||
"MessageEmbedFinished": "Osadzanie zakończone!",
|
||||
@@ -408,6 +471,7 @@
|
||||
"MessageNoEpisodes": "Brak odcinków",
|
||||
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
|
||||
"MessageNoGenres": "Brak gatunków",
|
||||
"MessageNoIssues": "Brak problemów",
|
||||
"MessageNoItems": "Brak elementów",
|
||||
"MessageNoItemsFound": "Nie znaleziono żadnych elemntów",
|
||||
"MessageNoListeningSessions": "Brak sesji odtwarzania",
|
||||
@@ -417,18 +481,28 @@
|
||||
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
|
||||
"MessageNoResults": "Brak wyników",
|
||||
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
|
||||
"MessageNoSeries": "No Series",
|
||||
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
|
||||
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
|
||||
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageOr": "lub",
|
||||
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
|
||||
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
|
||||
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
|
||||
"MessageRemoveAllItemsWarning": "UWAGA! Ta akcja usunie wszystkie elementy biblioteki z bazy danych, w tym wszystkie aktualizacje lub dopasowania, które zostały wykonane. Pliki pozostaną niezmienione. Czy jesteś pewien?",
|
||||
"MessageRemoveChapter": "Usuń rozdział",
|
||||
"MessageRemoveEpisodes": "Usuń {0} odcinków",
|
||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||
"MessageRemoveUserWarning": "Czy na pewno chcesz trwale usunąć użytkownika \"{0}\"?",
|
||||
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
|
||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
|
||||
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisane bazy danych w folderze /config oraz okładke w folderze /metadata/items & /metadata/authors.<br /><br />Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane<br /><br />Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani",
|
||||
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
|
||||
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
|
||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
|
||||
"MessageThinking": "Myślę...",
|
||||
"MessageUploaderItemFailed": "Nie udało się przesłać",
|
||||
@@ -436,11 +510,12 @@
|
||||
"MessageUploading": "Przesyłanie...",
|
||||
"MessageValidCronExpression": "Sprawdź wyrażenie CRON",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher jest wyłączony globalnie w ustawieniach serwera",
|
||||
"MessageXLibraryIsEmpty": "{0} Biblioteka jest pusta!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Czas trwania Twojego audiobooka jest dłuższy niż znaleziony czas trwania",
|
||||
"MessageYourAudiobookDurationIsShorter": "Czas trwania Twojego audiobooka jest krótszy niż znaleziony czas trwania",
|
||||
"NoteChangeRootPassword": "Tylko użytkownik root, może posiadać puste hasło",
|
||||
"NoteChapterEditorTimes": "Uwaga: Czas rozpoczęcia pierwszego rozdziału musi pozostać na poziomie 0:00, a czas rozpoczęcia ostatniego rozdziału nie może przekroczyć czasu trwania audiobooka.",
|
||||
"NoteFolderPicker": "Note: folders already mapped will not be shown",
|
||||
"NoteFolderPicker": "Uwaga: dotychczas zmapowane foldery nie zostaną wyświetlone",
|
||||
"NoteFolderPickerDebian": "Uwaga: Wybór folderu w instalcji opartej o system debian nie jest w pełni zaimplementowany. Powinieneś wprowadzić ścieżkę do swojej biblioteki bezpośrednio.",
|
||||
"NoteRSSFeedPodcastAppsHttps": "Ostrzeżenie: Większość aplikacji do obsługi podcastów wymaga, aby adres URL kanału RSS korzystał z protokołu HTTPS.",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Ostrzeżenie: 1 lub więcej odcinków nie ma daty publikacji. Niektóre aplikacje do słuchania podcastów tego wymagają.",
|
||||
@@ -449,6 +524,7 @@
|
||||
"NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.",
|
||||
"PlaceholderNewCollection": "Nowa nazwa kolekcji",
|
||||
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Szukanie..",
|
||||
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
|
||||
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
|
||||
@@ -460,8 +536,8 @@
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Autor zaktualizowany (nie znaleziono obrazu)",
|
||||
"ToastBackupCreateFailed": "Nie udało się utworzyć kopii zapasowej",
|
||||
"ToastBackupCreateSuccess": "Utworzono kopię zapasową",
|
||||
"ToastBackupDeleteFailed": "Failed to delete backup",
|
||||
"ToastBackupDeleteSuccess": "Nie udało się usunąć kopii zapasowej",
|
||||
"ToastBackupDeleteFailed": "Nie udało się usunąć kopii zapasowej",
|
||||
"ToastBackupDeleteSuccess": "Udało się usunąć kopie zapasowej",
|
||||
"ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej",
|
||||
"ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej",
|
||||
"ToastBackupUploadSuccess": "Kopia zapasowa została przesłana",
|
||||
@@ -473,6 +549,8 @@
|
||||
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
|
||||
"ToastBookmarkUpdateFailed": "Nie udało się zaktualizować zakładki",
|
||||
"ToastBookmarkUpdateSuccess": "Zaktualizowano zakładkę",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastCollectionItemsRemoveFailed": "Nie udało się usunąć pozycji z kolekcji",
|
||||
"ToastCollectionItemsRemoveSuccess": "Przedmiot(y) zostały usunięte z kolekcji",
|
||||
"ToastCollectionRemoveFailed": "Nie udało się usunąć kolekcji",
|
||||
@@ -496,14 +574,21 @@
|
||||
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
||||
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
|
||||
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
||||
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
||||
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
|
||||
"ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
|
||||
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
||||
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
|
||||
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
||||
"ToastSessionDeleteSuccess": "Sesja usunięta",
|
||||
"ToastSocketConnected": "Nawiązano połączenie z serwerem",
|
||||
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
||||
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
||||
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
||||
"ToastUserDeleteSuccess": "Użytkownik usunięty",
|
||||
"WeekdayFriday": "Piątek",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"ButtonAdd": "添加",
|
||||
"ButtonAddChapters": "添加章节",
|
||||
"ButtonAddPodcasts": "添加播客",
|
||||
"ButtonAddYourFirstLibrary": "添加第一个图书库",
|
||||
"ButtonAddYourFirstLibrary": "添加第一个媒体库",
|
||||
"ButtonApply": "应用",
|
||||
"ButtonApplyChapters": "应用到章节",
|
||||
"ButtonAuthors": "作者",
|
||||
@@ -13,8 +13,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "检查并下载新剧集",
|
||||
"ButtonChooseAFolder": "选择文件夹",
|
||||
"ButtonChooseFiles": "选择文件",
|
||||
"ButtonClearFilter": "清除过滤器",
|
||||
"ButtonCloseFeed": "关闭源",
|
||||
"ButtonCollections": "收藏",
|
||||
"ButtonConfigureScanner": "配置扫描",
|
||||
"ButtonCreate": "创建",
|
||||
"ButtonCreateBackup": "创建备份",
|
||||
"ButtonDelete": "删除",
|
||||
@@ -24,11 +26,11 @@
|
||||
"ButtonFullPath": "完整路径",
|
||||
"ButtonHide": "隐藏",
|
||||
"ButtonHome": "首页",
|
||||
"ButtonIssues": "反馈问题",
|
||||
"ButtonIssues": "问题",
|
||||
"ButtonLatest": "最新",
|
||||
"ButtonLibrary": "媒体库",
|
||||
"ButtonLogout": "注销",
|
||||
"ButtonLookup": "查找",
|
||||
"ButtonLibrary": "图书库",
|
||||
"ButtonManageTracks": "管理音轨",
|
||||
"ButtonMapChapterTitles": "章节标题结构",
|
||||
"ButtonMatchAllAuthors": "匹配所有作者",
|
||||
@@ -39,16 +41,17 @@
|
||||
"ButtonOpenManager": "打开管理器",
|
||||
"ButtonPlay": "播放",
|
||||
"ButtonPlaying": "正在播放",
|
||||
"ButtonPlaylists": "播放列表",
|
||||
"ButtonPurgeAllCache": "清理所有缓存",
|
||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||
"ButtonPurgeMediaProgress": "清理媒体进度",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQueueAddItem": "添加到队列",
|
||||
"ButtonQueueRemoveItem": "从队列中移除",
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
"ButtonRead": "读取",
|
||||
"ButtonRemove": "移除",
|
||||
"ButtonRemoveAll": "移除所有",
|
||||
"ButtonRemoveAllLibraryItems": "移除所有图书项目",
|
||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
||||
"ButtonReScan": "重新扫描",
|
||||
@@ -58,9 +61,11 @@
|
||||
"ButtonSaveAndClose": "保存并关闭",
|
||||
"ButtonSaveTracklist": "保存音轨列表",
|
||||
"ButtonScan": "扫描",
|
||||
"ButtonScanLibrary": "扫描库",
|
||||
"ButtonSearch": "查找",
|
||||
"ButtonSelectFolderPath": "选择文件夹路径",
|
||||
"ButtonSeries": "系列",
|
||||
"ButtonSetChaptersFromTracks": "将音轨设置为章节",
|
||||
"ButtonShiftTimes": "快速移动时间",
|
||||
"ButtonShow": "显示",
|
||||
"ButtonStartM4BEncode": "开始 M4B 编码",
|
||||
@@ -92,9 +97,9 @@
|
||||
"HeaderItemFiles": "项目文件",
|
||||
"HeaderLastListeningSession": "最后一次收听会话",
|
||||
"HeaderLatestEpisodes": "最新剧集",
|
||||
"HeaderLibraries": "图书库",
|
||||
"HeaderLibraryFiles": "图书库文件",
|
||||
"HeaderLibraryStats": "图书库统计数据",
|
||||
"HeaderLibraries": "媒体库",
|
||||
"HeaderLibraryFiles": "媒体库文件",
|
||||
"HeaderLibraryStats": "媒体库统计数据",
|
||||
"HeaderListeningSessions": "收听会话",
|
||||
"HeaderListeningStats": "收听统计数据",
|
||||
"HeaderLogin": "登录",
|
||||
@@ -102,12 +107,14 @@
|
||||
"HeaderMatch": "匹配",
|
||||
"HeaderMetadataToEmbed": "嵌入元数据",
|
||||
"HeaderNewAccount": "新建帐户",
|
||||
"HeaderNewLibrary": "新建图书库",
|
||||
"HeaderNewLibrary": "新建媒体库",
|
||||
"HeaderNotifications": "通知",
|
||||
"HeaderOtherFiles": "其他文件",
|
||||
"HeaderOpenRSSFeed": "打开 RSS 源",
|
||||
"HeaderOtherFiles": "其他文件",
|
||||
"HeaderPermissions": "权限",
|
||||
"HeaderPlayerQueue": "Player Queue",
|
||||
"HeaderPlayerQueue": "播放队列",
|
||||
"HeaderPlaylist": "播放列表",
|
||||
"HeaderPlaylistItems": "播放列表项目",
|
||||
"HeaderPodcastsToAdd": "要添加的播客",
|
||||
"HeaderPreviewCover": "预览封面",
|
||||
"HeaderRemoveEpisode": "移除剧集",
|
||||
@@ -115,7 +122,7 @@
|
||||
"HeaderRSSFeedIsOpen": "RSS 源已打开",
|
||||
"HeaderSavedMediaProgress": "保存媒体进度",
|
||||
"HeaderSchedule": "计划任务",
|
||||
"HeaderScheduleLibraryScans": "自动扫描图书库",
|
||||
"HeaderScheduleLibraryScans": "自动扫描媒体库",
|
||||
"HeaderSession": "会话",
|
||||
"HeaderSetBackupSchedule": "设置备份计划任务",
|
||||
"HeaderSettings": "设置",
|
||||
@@ -133,7 +140,7 @@
|
||||
"HeaderUpdateAccount": "更新帐户",
|
||||
"HeaderUpdateAuthor": "更新作者",
|
||||
"HeaderUpdateDetails": "更新详情",
|
||||
"HeaderUpdateLibrary": "更新图书库",
|
||||
"HeaderUpdateLibrary": "更新媒体库",
|
||||
"HeaderUsers": "用户",
|
||||
"HeaderYourStats": "你的统计数据",
|
||||
"LabelAccountType": "帐户类型",
|
||||
@@ -141,10 +148,16 @@
|
||||
"LabelAccountTypeGuest": "来宾",
|
||||
"LabelAccountTypeUser": "用户",
|
||||
"LabelActivity": "活动",
|
||||
"LabelAddedAt": "添加于",
|
||||
"LabelAddToCollection": "添加到收藏",
|
||||
"LabelAddToCollectionBatch": "添加 {0} 图书到收藏",
|
||||
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
||||
"LabelAddToPlaylist": "添加到播放列表",
|
||||
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
||||
"LabelAll": "全部",
|
||||
"LabelAllUsers": "所有用户",
|
||||
"LabelAuthor": "作者",
|
||||
"LabelAuthorFirstLast": "作者 (姓 名)",
|
||||
"LabelAuthorLastFirst": "作者 (名, 姓)",
|
||||
"LabelAuthors": "作者",
|
||||
"LabelAutoDownloadEpisodes": "自动下载剧集",
|
||||
"LabelBackToUser": "返回到用户",
|
||||
@@ -158,9 +171,10 @@
|
||||
"LabelChangePassword": "修改密码",
|
||||
"LabelChaptersFound": "找到的章节",
|
||||
"LabelChapterTitle": "章节标题",
|
||||
"LabelClosePlayer": "关闭播放器",
|
||||
"LabelCollapseSeries": "折叠系列",
|
||||
"LabelCollections": "收藏",
|
||||
"LabelComplete": "完成",
|
||||
"LabelComplete": "已完成",
|
||||
"LabelConfirmPassword": "确认密码",
|
||||
"LabelContinueListening": "继续收听",
|
||||
"LabelContinueSeries": "继续收听系列",
|
||||
@@ -187,23 +201,34 @@
|
||||
"LabelEpisode": "剧集",
|
||||
"LabelEpisodeTitle": "剧集标题",
|
||||
"LabelEpisodeType": "剧集类型",
|
||||
"LabelExplicit": "显式",
|
||||
"LabelExplicit": "信息明确",
|
||||
"LabelFeedURL": "源 URL",
|
||||
"LabelFile": "文件",
|
||||
"LabelFileBirthtime": "文件创建时间",
|
||||
"LabelFileModified": "文件修改时间",
|
||||
"LabelFilename": "文件名",
|
||||
"LabelFilterByUser": "按用户筛选",
|
||||
"LabelFindEpisodes": "查找剧集",
|
||||
"LabelFinished": "听完",
|
||||
"LabelFinished": "已听完",
|
||||
"LabelFolder": "文件夹",
|
||||
"LabelFolders": "文件夹",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
"LabelHardDeleteFile": "完全删除文件",
|
||||
"LabelHour": "小时",
|
||||
"LabelIcon": "图标",
|
||||
"LabelIncludeInTracklist": "包含在音轨列表中",
|
||||
"LabelIncomplete": "不完整",
|
||||
"LabelInProgress": "正在进行",
|
||||
"LabelIncomplete": "未听完",
|
||||
"LabelInProgress": "正在听",
|
||||
"LabelInterval": "间隔",
|
||||
"LabelIntervalCustomDailyWeekly": "自定义 每天 / 每周",
|
||||
"LabelIntervalEvery12Hours": "每 12 小时",
|
||||
"LabelIntervalEvery15Minutes": "每 15 分钟",
|
||||
"LabelIntervalEvery2Hours": "每 2 小时",
|
||||
"LabelIntervalEvery30Minutes": "每 30 分钟",
|
||||
"LabelIntervalEvery6Hours": "每 6 小时",
|
||||
"LabelIntervalEveryDay": "每天",
|
||||
"LabelIntervalEveryHour": "每小时",
|
||||
"LabelInvalidParts": "无效部件",
|
||||
"LabelItem": "项目",
|
||||
"LabelLanguage": "语言",
|
||||
@@ -212,12 +237,15 @@
|
||||
"LabelLastTime": "最近一次",
|
||||
"LabelLastUpdate": "最近更新",
|
||||
"LabelLess": "较少",
|
||||
"LabelLibrariesAccessibleToUser": "用户可访问的图书库",
|
||||
"LabelLibrary": "图书库",
|
||||
"LabelLibraryItem": "图书库项目",
|
||||
"LabelLibraryName": "图书库名称",
|
||||
"LabelLimit": "限度",
|
||||
"LabelLibrariesAccessibleToUser": "用户可访问的媒体库",
|
||||
"LabelLibrary": "媒体库",
|
||||
"LabelLibraryItem": "媒体库项目",
|
||||
"LabelLibraryName": "媒体库名称",
|
||||
"LabelLimit": "限制",
|
||||
"LabelListenAgain": "再次收听",
|
||||
"LabelLogLevelDebug": "调试",
|
||||
"LabelLogLevelInfo": "信息",
|
||||
"LabelLogLevelWarn": "警告",
|
||||
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
|
||||
"LabelMarkSeries": "标记系列",
|
||||
"LabelMediaPlayer": "媒体播放器",
|
||||
@@ -229,26 +257,30 @@
|
||||
"LabelMissingParts": "丢失的部分",
|
||||
"LabelMore": "更多",
|
||||
"LabelName": "名称",
|
||||
"LabelNarrator": "演播者",
|
||||
"LabelNarrators": "演播者",
|
||||
"LabelNew": "新建",
|
||||
"LabelNewestAuthors": "最新作者",
|
||||
"LabelNewestEpisodes": "最新剧集",
|
||||
"LabelNewPassword": "新密码",
|
||||
"LabelNotes": "注意",
|
||||
"LabelNotFinished": "未完成",
|
||||
"LabelNotificationEvent": "通知事件",
|
||||
"LabelNotes": "注释",
|
||||
"LabelNotFinished": "未听完",
|
||||
"LabelNotificationAppriseURL": "通知 URL(s)",
|
||||
"LabelNotificationAvailableVariables": "可用变量",
|
||||
"LabelNotificationBodyTemplate": "正文模板",
|
||||
"LabelNotificationTitleTemplate": "标题模板",
|
||||
"LabelNotificationEvent": "通知事件",
|
||||
"LabelNotificationsMaxFailedAttempts": "最大失败尝试次数",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "如果多次发送失败,通知将被禁用",
|
||||
"LabelNotificationsMaxQueueSize": "通知事件的最大队列大小",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制为每秒触发 1 个. 如果队列处于最大大小, 则将忽略事件. 这可以防止通知垃圾邮件.",
|
||||
"LabelNotificationTitleTemplate": "标题模板",
|
||||
"LabelNotStarted": "未开始",
|
||||
"LabelNumberOfBooks": "图书数量",
|
||||
"LabelNumberOfEpisodes": "# 集",
|
||||
"LabelOpenRSSFeed": "打开 RSS 源",
|
||||
"LabelPassword": "密码",
|
||||
"LabelPath": "路径",
|
||||
"LabelPermissionsAccessAllLibraries": "可以访问所有图书库",
|
||||
"LabelPermissionsAccessAllLibraries": "可以访问所有媒体库",
|
||||
"LabelPermissionsAccessAllTags": "可以访问所有标签",
|
||||
"LabelPermissionsAccessExplicitContent": "可以访问显式内容",
|
||||
"LabelPermissionsDelete": "可以删除",
|
||||
@@ -256,6 +288,7 @@
|
||||
"LabelPermissionsUpdate": "可以更新",
|
||||
"LabelPermissionsUpload": "可以上传",
|
||||
"LabelPhotoPathURL": "图片路径或 URL",
|
||||
"LabelPlaylists": "播放列表",
|
||||
"LabelPlayMethod": "播放方法",
|
||||
"LabelPodcast": "播客",
|
||||
"LabelPodcasts": "播客",
|
||||
@@ -269,6 +302,8 @@
|
||||
"LabelRecentSeries": "最近添加系列",
|
||||
"LabelRegion": "区域",
|
||||
"LabelReleaseDate": "发布日期",
|
||||
"LabelRemoveCover": "移除封面",
|
||||
"LabelRSSFeedOpen": "打开 RSS 源",
|
||||
"LabelRSSFeedSlug": "RSS 源段",
|
||||
"LabelRSSFeedURL": "RSS 源 URL",
|
||||
"LabelSearchTerm": "搜索项",
|
||||
@@ -278,11 +313,12 @@
|
||||
"LabelSequence": "序列",
|
||||
"LabelSeries": "系列",
|
||||
"LabelSeriesName": "系列名称",
|
||||
"LabelSeriesProgress": "系列进度",
|
||||
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
|
||||
"LabelSettingsChromecastSupport": "Chromecast 支持",
|
||||
"LabelSettingsDateFormat": "日期格式",
|
||||
"LabelSettingsDisableWatcher": "禁用监视程序",
|
||||
"LabelSettingsDisableWatcherForLibrary": "禁用图书库的文件夹监视程序",
|
||||
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序",
|
||||
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
|
||||
"LabelSettingsEnableEReader": "为所有用户启用电子阅读器",
|
||||
"LabelSettingsEnableEReaderHelp": "电子阅读器仍在开发中,但可以使用此设置向所有用户打开它(或使用 \"实验功能\" 切换仅供你使用)",
|
||||
@@ -291,29 +327,30 @@
|
||||
"LabelSettingsFindCovers": "查找封面",
|
||||
"LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.<br>注意: 这将延长扫描时间",
|
||||
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
|
||||
"LabelSettingsLibraryBookshelfView": "图书库使用书架视图",
|
||||
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
|
||||
"LabelSettingsOverdriveMediaMarkers": "对章节使用 Overdrive 媒体标记",
|
||||
"LabelSettingsOverdriveMediaMarkersHelp": "Overdrive 的 MP3 文件带有作为自定义元数据嵌入的章节时间. 启用此功能将自动将这些标签用于章节计时",
|
||||
"LabelSettingsParseSubtitles": "解析副标题",
|
||||
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
|
||||
"LabelSettingsPreferAudioMetadata": "首选音频元数据",
|
||||
"LabelSettingsPreferAudioMetadataHelp": "音频文件 ID3 元标记将用于文件夹名称上图书的详细信息",
|
||||
"LabelSettingsPreferAudioMetadataHelp": "音频文件 ID3 元标记将用于文件夹名称上媒体的详细信息",
|
||||
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配时, 匹配的数据将覆盖项目详细信息. 默认情况下, 快速匹配将只填充缺少的详细信息.",
|
||||
"LabelSettingsPreferOPFMetadata": "首选 OPF 元数据",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "OPF 文件元数据将用于文件夹名称上图书的详细信息",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "OPF 文件元数据将用于文件夹名称上媒体的详细信息",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "跳过匹配已有 ASIN 的图书",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "跳过匹配已有 ISBN 的图书",
|
||||
"LabelSettingsSortingIgnorePrefixes": "排序时忽略前缀",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "例如: 前缀为 \"The\" 的图书标题 \"The Book Title\" 将按 \"Book Title, The\" 进行排序",
|
||||
"LabelSettingsStoreCoversWithItem": "存储项目封面",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你图书项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
||||
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你图书项目文件夹中. 使 .abs 文件护展名",
|
||||
"LabelSettingsSquareBookCovers": "用户方形图书封面",
|
||||
"LabelSettingsSquareBookCoversHelp": "比起标准的 1.6:1 图书封面,更喜欢使用方形封面",
|
||||
"LabelSettingsStoreCoversWithItem": "存储项目封面",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
||||
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名",
|
||||
"LabelShowAll": "全部显示",
|
||||
"LabelSize": "大小",
|
||||
"LabelSize": "文件大小",
|
||||
"LabelSleepTimer": "睡眠定时",
|
||||
"LabelStart": "开始",
|
||||
"LabelStarted": "开始于",
|
||||
"LabelStartedAt": "从这开始",
|
||||
@@ -327,13 +364,15 @@
|
||||
"LabelStatsHours": "小时",
|
||||
"LabelStatsInARow": "在一行",
|
||||
"LabelStatsItemsFinished": "已完成的项目",
|
||||
"LabelStatsItemsInLibrary": "图书库中的项目",
|
||||
"LabelStatsItemsInLibrary": "媒体库中的项目",
|
||||
"LabelStatsMinutes": "分钟",
|
||||
"LabelStatsMinutesListening": "收听分钟数",
|
||||
"LabelStatsOverall": "总计",
|
||||
"LabelStatsOverallDays": "总计天数",
|
||||
"LabelStatsOverallHours": "总计小时",
|
||||
"LabelStatsWeekListening": "每周收听",
|
||||
"LabelSubtitle": "副标题",
|
||||
"LabelSupportedFileTypes": "支持的文件类型",
|
||||
"LabelTag": "标签",
|
||||
"LabelTags": "标签",
|
||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||
"LabelTimeListened": "收听时间",
|
||||
@@ -341,9 +380,19 @@
|
||||
"LabelTimeRemaining": "剩余 {0}",
|
||||
"LabelTimeToShift": "快速移动时间以秒为单位",
|
||||
"LabelTitle": "标题",
|
||||
"LabelToolsEmbedMetadata": "嵌入元数据",
|
||||
"LabelToolsEmbedMetadataDescription": "将元数据嵌入音频文件, 包括封面图像和章节.",
|
||||
"LabelToolsMakeM4b": "制作 M4B 有声读物文件",
|
||||
"LabelToolsMakeM4bDescription": "生成带有嵌入元数据, 封面图像和章节的 .M4B 有声读物文件.",
|
||||
"LabelToolsSplitM4b": "将 M4B 文件拆分为 MP3 文件",
|
||||
"LabelToolsSplitM4bDescription": "从 M4B 文件创建 MP3 文件, 按章节分割, 并嵌入元数据, 封面图像和章节.",
|
||||
"LabelTotalDuration": "总持续时间",
|
||||
"LabelTotalTimeListened": "总收听时间",
|
||||
"LabelTrackFromFilename": "从文件名获取音轨",
|
||||
"LabelTrackFromMetadata": "从源数据获取音轨",
|
||||
"LabelTracks": "音轨",
|
||||
"LabelTracksMultiTrack": "多轨",
|
||||
"LabelTracksSingleTrack": "单轨",
|
||||
"LabelType": "类型",
|
||||
"LabelUnknown": "未知",
|
||||
"LabelUpdateCover": "更新封面",
|
||||
@@ -359,23 +408,37 @@
|
||||
"LabelUsername": "用户名",
|
||||
"LabelValue": "值",
|
||||
"LabelVersion": "版本",
|
||||
"LabelViewBookmarks": "查看书签",
|
||||
"LabelViewChapters": "查看章节",
|
||||
"LabelViewQueue": "查看播放列表",
|
||||
"LabelVolume": "音量",
|
||||
"LabelWeekdaysToRun": "工作日运行",
|
||||
"LabelYourAudiobookDuration": "你的有声读物持续时间",
|
||||
"LabelYourBookmarks": "你的书签",
|
||||
"LabelYourPlaylists": "你的播放列表",
|
||||
"LabelYourProgress": "你的进度",
|
||||
"MessageBackupsDescription": "备份包括用户, 用户进度, 图书库项目详细信息, 服务器设置和图像, 存储在",
|
||||
"MessageBackupsNote": "备份不包括存储在您的图书库文件夹中的任何文件.",
|
||||
"MessageAddToPlayerQueue": "添加到播放队列",
|
||||
"MessageAppriseDescription": "要使用此功能,您需要运行一个 <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> 实例或一个可以处理这些相同请求的 API. <br />Apprise API Url 应该是发送通知的完整 URL 路径, 例如: 如果你的 API 实例运行在 <code>http://192.168.1.1:8337</code>, 那么你可以输入 <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "备份包括用户, 用户进度, 媒体库项目详细信息, 服务器设置和图像, 存储在 <code>/metadata/items</code> & <code>/metadata/authors</code>. 备份不包括存储在您的媒体库文件夹中的任何文件.",
|
||||
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
|
||||
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
|
||||
"MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源",
|
||||
"MessageBookshelfNoSeries": "你没有系列",
|
||||
"MessageChapterEndIsAfter": "章节结束是在有声读物结束之后",
|
||||
"MessageChapterErrorFirstNotZero": "第一章节必须从 0 开始",
|
||||
"MessageChapterErrorStartGteDuration": "无效的开始时间, 必须小于有声读物持续时间",
|
||||
"MessageChapterErrorStartLtPrev": "无效的开始时间, 必须大于或等于上一章节的开始时间",
|
||||
"MessageChapterStartIsAfter": "章节开始是在有声读物结束之后",
|
||||
"MessageCheckingCron": "检查计划任务...",
|
||||
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除图书库 \"{0}\"?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||
"MessageDownloadingEpisode": "正在下载剧集",
|
||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||
"MessageEmbedFinished": "嵌入完成!",
|
||||
@@ -395,7 +458,7 @@
|
||||
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
||||
"MessageMarkAsFinished": "标记为已听完",
|
||||
"MessageMarkAsNotFinished": "标记为未听完",
|
||||
"MessageMatchBooksDescription": "尝试将图书库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||
"MessageNoAudioTracks": "没有音轨",
|
||||
"MessageNoAuthors": "没有作者",
|
||||
"MessageNoBackups": "没有备份",
|
||||
@@ -408,6 +471,7 @@
|
||||
"MessageNoEpisodes": "没有剧集",
|
||||
"MessageNoFoldersAvailable": "没有可用文件夹",
|
||||
"MessageNoGenres": "无流派",
|
||||
"MessageNoIssues": "无问题",
|
||||
"MessageNoItems": "无项目",
|
||||
"MessageNoItemsFound": "未找到任何项目",
|
||||
"MessageNoListeningSessions": "无收听会话",
|
||||
@@ -417,38 +481,50 @@
|
||||
"MessageNoPodcastsFound": "未找到播客",
|
||||
"MessageNoResults": "无结果",
|
||||
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
||||
"MessageNoSeries": "无系列",
|
||||
"MessageNotYetImplemented": "尚未实施",
|
||||
"MessageNoUpdateNecessary": "无需更新",
|
||||
"MessageNoUpdatesWereNecessary": "无需更新",
|
||||
"MessageNoUserPlaylists": "你没有播放列表",
|
||||
"MessageOr": "或",
|
||||
"MessagePauseChapter": "暂停章节播放",
|
||||
"MessagePlayChapter": "开始章节播放",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
|
||||
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
|
||||
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的图书库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
|
||||
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的媒体库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
|
||||
"MessageRemoveChapter": "移除章节",
|
||||
"MessageRemoveEpisodes": "移除 {0} 剧集",
|
||||
"MessageRemoveFromPlayerQueue": "从播放队列中移除",
|
||||
"MessageRemoveUserWarning": "是否确实要永久删除用户 \"{0}\"?",
|
||||
"MessageReportBugsAndContribute": "报告错误、请求功能和贡献在",
|
||||
"MessageRestoreBackupConfirm": "您确定要恢复创建的这个备份",
|
||||
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改图书库文件夹中的任何文件. 如果您已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.",
|
||||
"MessageResetChaptersConfirm": "你确定要重置章节并撤消你所做的更改吗?",
|
||||
"MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份",
|
||||
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改媒体库文件夹中的任何文件. 如果您已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.",
|
||||
"MessageSearchResultsFor": "搜索结果",
|
||||
"MessageServerCouldNotBeReached": "无法访问服务器",
|
||||
"MessageSetChaptersFromTracksDescription": "把每个音频文件设置为章节并将章节标题设置为音频文件名",
|
||||
"MessageStartPlaybackAtTime": "开始播放 \"{0}\" 在 {1}?",
|
||||
"MessageThinking": "思考...",
|
||||
"MessageThinking": "正在查找...",
|
||||
"MessageUploaderItemFailed": "上传失败",
|
||||
"MessageUploaderItemSuccess": "上传成功!",
|
||||
"MessageUploading": "正在上传...",
|
||||
"MessageValidCronExpression": "有效的计划任务表达式",
|
||||
"MessageWatcherIsDisabledGlobally": "在服务器设置中禁用全局监视程序",
|
||||
"MessageXLibraryIsEmpty": "{0} 库为空!",
|
||||
"MessageYourAudiobookDurationIsLonger": "您的有声读物持续时间比找到的持续时间长",
|
||||
"MessageYourAudiobookDurationIsShorter": "您的有声读物持续时间比找到的持续时间短",
|
||||
"NoteChangeRootPassword": "Root 是唯一可以拥有空密码的用户",
|
||||
"NoteChapterEditorTimes": "注意: 第一章开始时间必须保持在 0:00, 最后一章开始时间不能超过有声读物持续时间.",
|
||||
"NoteFolderPicker": "注意: 将不显示已映射的文件夹",
|
||||
"NoteFolderPickerDebian": "注意: debian 安装的文件夹选择器尚未完全实现. 您应该直接输入图书库的路径.",
|
||||
"NoteFolderPickerDebian": "注意: debian 安装的文件夹选择器尚未完全实现. 您应该直接输入媒体库的路径.",
|
||||
"NoteRSSFeedPodcastAppsHttps": "警告: 大多数播客应用程序都需要 RSS 源 URL 使用 HTTPS",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集没有发布日期. 一些播客应用程序要求这样做.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的图书库项目处理.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的媒体库项目处理.",
|
||||
"NoteUploaderOnlyAudioFiles": "如果只上传音频文件, 则每个音频文件将作为单独的有声读物处理.",
|
||||
"NoteUploaderUnsupportedFiles": "不支持的文件将被忽略. 选择或删除文件夹时, 将忽略不在项目文件夹中的其他文件.",
|
||||
"PlaceholderNewCollection": "新建收藏夹名称",
|
||||
"PlaceholderNewCollection": "输入收藏夹名称",
|
||||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||
"PlaceholderSearch": "查找..",
|
||||
"ToastAccountUpdateFailed": "账户更新失败",
|
||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||
@@ -473,6 +549,8 @@
|
||||
"ToastBookmarkRemoveSuccess": "书签已删除",
|
||||
"ToastBookmarkUpdateFailed": "书签更新失败",
|
||||
"ToastBookmarkUpdateSuccess": "书签已更新",
|
||||
"ToastChaptersHaveErrors": "章节有错误",
|
||||
"ToastChaptersMustHaveTitles": "章节必须有标题",
|
||||
"ToastCollectionItemsRemoveFailed": "从收藏夹移除项目失败",
|
||||
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
|
||||
"ToastCollectionRemoveFailed": "删除收藏夹失败",
|
||||
@@ -488,22 +566,29 @@
|
||||
"ToastItemMarkedAsFinishedSuccess": "标记为听完的项目",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "标记为未听完失败",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目",
|
||||
"ToastLibraryCreateFailed": "创建图书库失败",
|
||||
"ToastLibraryCreateSuccess": "图书库 \"{0}\" 创建成功",
|
||||
"ToastLibraryDeleteFailed": "删除图书库失败",
|
||||
"ToastLibraryDeleteSuccess": "图书库已删除",
|
||||
"ToastLibraryCreateFailed": "创建媒体库失败",
|
||||
"ToastLibraryCreateSuccess": "媒体库 \"{0}\" 创建成功",
|
||||
"ToastLibraryDeleteFailed": "删除媒体库失败",
|
||||
"ToastLibraryDeleteSuccess": "媒体库已删除",
|
||||
"ToastLibraryScanFailedToStart": "无法启动扫描",
|
||||
"ToastLibraryScanStarted": "图书库扫描已启动",
|
||||
"ToastLibraryScanStarted": "媒体库扫描已启动",
|
||||
"ToastLibraryUpdateFailed": "更新图书库失败",
|
||||
"ToastLibraryUpdateSuccess": "图书库 \"{0}\" 已更新",
|
||||
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
|
||||
"ToastPlaylistRemoveFailed": "删除播放列表失败",
|
||||
"ToastPlaylistRemoveSuccess": "播放列表已删除",
|
||||
"ToastPlaylistUpdateFailed": "更新播放列表失败",
|
||||
"ToastPlaylistUpdateSuccess": "播放列表已更新",
|
||||
"ToastPodcastCreateFailed": "创建播客失败",
|
||||
"ToastPodcastCreateSuccess": "已成功创建播客",
|
||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||
"ToastRemoveItemFromCollectionFailed": "从收藏中删除项目失败",
|
||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||
"ToastSessionDeleteFailed": "删除会话失败",
|
||||
"ToastSessionDeleteSuccess": "会话已删除",
|
||||
"ToastSocketConnected": "网络已连接",
|
||||
"ToastSocketDisconnected": "网络已断开",
|
||||
"ToastSocketFailedToConnect": "网络连接失败",
|
||||
"ToastUserDeleteFailed": "删除用户失败",
|
||||
"ToastUserDeleteSuccess": "用户已删除",
|
||||
"WeekdayFriday": "星期五",
|
||||
@@ -513,4 +598,4 @@
|
||||
"WeekdayThursday": "星期四",
|
||||
"WeekdayTuesday": "星期二",
|
||||
"WeekdayWednesday": "星期三"
|
||||
}
|
||||
}
|
||||
|
||||
176
package-lock.json
generated
176
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.4",
|
||||
"version": "2.2.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.4",
|
||||
"version": "2.2.7",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
@@ -24,10 +24,10 @@
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/component-emitter": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
|
||||
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
@@ -40,9 +40,9 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
@@ -112,9 +112,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||
"integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
@@ -124,7 +124,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.10.3",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -203,11 +203,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -405,9 +400,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.3.0.tgz",
|
||||
"integrity": "sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
@@ -429,13 +424,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
|
||||
"integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.0",
|
||||
"body-parser": "1.20.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
@@ -454,7 +449,7 @@
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.10.3",
|
||||
"qs": "6.11.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
@@ -499,9 +494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -553,13 +548,13 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -937,9 +932,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
@@ -1104,16 +1099,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
"integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.2.0",
|
||||
"socket.io-adapter": "~2.4.0",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
"socket.io-parser": "~4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -1125,12 +1120,11 @@
|
||||
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
|
||||
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
|
||||
"dependencies": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1252,7 +1246,7 @@
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -1260,7 +1254,7 @@
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
@@ -1268,7 +1262,7 @@
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -1315,10 +1309,10 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/component-emitter": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
|
||||
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
@@ -1331,9 +1325,9 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
@@ -1391,9 +1385,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||
"integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
@@ -1403,7 +1397,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.10.3",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -1458,11 +1452,6 @@
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1605,9 +1594,9 @@
|
||||
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
|
||||
},
|
||||
"entities": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.3.0.tgz",
|
||||
"integrity": "sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg=="
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA=="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
@@ -1620,13 +1609,13 @@
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
|
||||
"integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.0",
|
||||
"body-parser": "1.20.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
@@ -1645,7 +1634,7 @@
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.10.3",
|
||||
"qs": "6.11.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
@@ -1681,9 +1670,9 @@
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.2.0",
|
||||
@@ -1708,13 +1697,13 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
@@ -1984,9 +1973,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
@@ -2108,16 +2097,16 @@
|
||||
}
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
"integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.2.0",
|
||||
"socket.io-adapter": "~2.4.0",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
"socket.io-parser": "~4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@@ -2141,12 +2130,11 @@
|
||||
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
|
||||
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -2220,17 +2208,17 @@
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.2.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.4",
|
||||
"version": "2.2.7",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const bcrypt = require('./libs/bcryptjs')
|
||||
const jwt = require('./libs/jsonwebtoken')
|
||||
const requestIp = require('./libs/requestIp')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
class Auth {
|
||||
@@ -108,7 +109,7 @@ class Auth {
|
||||
Logger.error('JWT Verify Token Failed', err)
|
||||
return resolve(null)
|
||||
}
|
||||
var user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||
const user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||
resolve(user || null)
|
||||
})
|
||||
})
|
||||
@@ -125,14 +126,16 @@ class Auth {
|
||||
}
|
||||
|
||||
async login(req, res, feeds) {
|
||||
const ipAddress = requestIp.getClientIp(req)
|
||||
var username = (req.body.username || '').toLowerCase()
|
||||
var password = req.body.password || ''
|
||||
|
||||
var user = this.users.find(u => u.username.toLowerCase() === username)
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||
if (req.rateLimit.remaining <= 2) {
|
||||
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
||||
return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
|
||||
}
|
||||
return res.status(401).send('Invalid user or password')
|
||||
@@ -152,9 +155,9 @@ class Auth {
|
||||
if (compare) {
|
||||
res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||
} else {
|
||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||
if (req.rateLimit.remaining <= 2) {
|
||||
Logger.error(`[Auth] Failed login attempt for user ${user.username}. Attempts: ${req.rateLimit.current}`)
|
||||
Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
||||
return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`)
|
||||
}
|
||||
return res.status(401).send('Invalid user or password')
|
||||
|
||||
63
server/Db.js
63
server/Db.js
@@ -5,6 +5,7 @@ const { version } = require('../package.json')
|
||||
const LibraryItem = require('./objects/LibraryItem')
|
||||
const User = require('./objects/user/User')
|
||||
const Collection = require('./objects/Collection')
|
||||
const Playlist = require('./objects/Playlist')
|
||||
const Library = require('./objects/Library')
|
||||
const Author = require('./objects/entities/Author')
|
||||
const Series = require('./objects/entities/Series')
|
||||
@@ -20,6 +21,7 @@ class Db {
|
||||
this.LibrariesPath = Path.join(global.ConfigPath, 'libraries')
|
||||
this.SettingsPath = Path.join(global.ConfigPath, 'settings')
|
||||
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
|
||||
this.PlaylistsPath = Path.join(global.ConfigPath, 'playlists')
|
||||
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
|
||||
this.SeriesPath = Path.join(global.ConfigPath, 'series')
|
||||
this.FeedsPath = Path.join(global.ConfigPath, 'feeds')
|
||||
@@ -31,6 +33,7 @@ class Db {
|
||||
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.playlistsDb = new njodb.Database(this.PlaylistsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath, { lockoptions: { stale: staleTime } })
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
@@ -40,6 +43,7 @@ class Db {
|
||||
this.libraries = []
|
||||
this.settings = []
|
||||
this.collections = []
|
||||
this.playlists = []
|
||||
this.authors = []
|
||||
this.series = []
|
||||
|
||||
@@ -61,6 +65,7 @@ class Db {
|
||||
else if (entityName === 'library') return this.librariesDb
|
||||
else if (entityName === 'settings') return this.settingsDb
|
||||
else if (entityName === 'collection') return this.collectionsDb
|
||||
else if (entityName === 'playlist') return this.playlistsDb
|
||||
else if (entityName === 'author') return this.authorsDb
|
||||
else if (entityName === 'series') return this.seriesDb
|
||||
else if (entityName === 'feed') return this.feedsDb
|
||||
@@ -74,6 +79,7 @@ class Db {
|
||||
else if (entityName === 'library') return 'libraries'
|
||||
else if (entityName === 'settings') return 'settings'
|
||||
else if (entityName === 'collection') return 'collections'
|
||||
else if (entityName === 'playlist') return 'playlists'
|
||||
else if (entityName === 'author') return 'authors'
|
||||
else if (entityName === 'series') return 'series'
|
||||
else if (entityName === 'feed') return 'feeds'
|
||||
@@ -81,15 +87,17 @@ class Db {
|
||||
}
|
||||
|
||||
reinit() {
|
||||
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath)
|
||||
this.usersDb = new njodb.Database(this.UsersPath)
|
||||
this.sessionsDb = new njodb.Database(this.SessionsPath)
|
||||
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
|
||||
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath)
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
|
||||
const staleTime = 1000 * 60 * 2
|
||||
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath, { lockoptions: { stale: staleTime } })
|
||||
this.usersDb = new njodb.Database(this.UsersPath, { lockoptions: { stale: staleTime } })
|
||||
this.sessionsDb = new njodb.Database(this.SessionsPath, { lockoptions: { stale: staleTime } })
|
||||
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.playlistsDb = new njodb.Database(this.PlaylistsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath, { lockoptions: { stale: staleTime } })
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
return this.init()
|
||||
}
|
||||
|
||||
@@ -135,20 +143,20 @@ class Db {
|
||||
}
|
||||
|
||||
async load() {
|
||||
var p1 = this.libraryItemsDb.select(() => true).then((results) => {
|
||||
const p1 = this.libraryItemsDb.select(() => true).then((results) => {
|
||||
this.libraryItems = results.data.map(a => new LibraryItem(a))
|
||||
Logger.info(`[DB] ${this.libraryItems.length} Library Items Loaded`)
|
||||
})
|
||||
var p2 = this.usersDb.select(() => true).then((results) => {
|
||||
const p2 = this.usersDb.select(() => true).then((results) => {
|
||||
this.users = results.data.map(u => new User(u))
|
||||
Logger.info(`[DB] ${this.users.length} Users Loaded`)
|
||||
})
|
||||
var p3 = this.librariesDb.select(() => true).then((results) => {
|
||||
const p3 = this.librariesDb.select(() => true).then((results) => {
|
||||
this.libraries = results.data.map(l => new Library(l))
|
||||
this.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
Logger.info(`[DB] ${this.libraries.length} Libraries Loaded`)
|
||||
})
|
||||
var p4 = this.settingsDb.select(() => true).then(async (results) => {
|
||||
const p4 = this.settingsDb.select(() => true).then(async (results) => {
|
||||
if (results.data && results.data.length) {
|
||||
this.settings = results.data
|
||||
var serverSettings = this.settings.find(s => s.id === 'server-settings')
|
||||
@@ -179,19 +187,23 @@ class Db {
|
||||
}
|
||||
}
|
||||
})
|
||||
var p5 = this.collectionsDb.select(() => true).then((results) => {
|
||||
const p5 = this.collectionsDb.select(() => true).then((results) => {
|
||||
this.collections = results.data.map(l => new Collection(l))
|
||||
Logger.info(`[DB] ${this.collections.length} Collections Loaded`)
|
||||
})
|
||||
var p6 = this.authorsDb.select(() => true).then((results) => {
|
||||
const p6 = this.playlistsDb.select(() => true).then((results) => {
|
||||
this.playlists = results.data.map(l => new Playlist(l))
|
||||
Logger.info(`[DB] ${this.playlists.length} Playlists Loaded`)
|
||||
})
|
||||
const p7 = this.authorsDb.select(() => true).then((results) => {
|
||||
this.authors = results.data.map(l => new Author(l))
|
||||
Logger.info(`[DB] ${this.authors.length} Authors Loaded`)
|
||||
})
|
||||
var p7 = this.seriesDb.select(() => true).then((results) => {
|
||||
const p8 = this.seriesDb.select(() => true).then((results) => {
|
||||
this.series = results.data.map(l => new Series(l))
|
||||
Logger.info(`[DB] ${this.series.length} Series Loaded`)
|
||||
})
|
||||
await Promise.all([p1, p2, p3, p4, p5, p6, p7])
|
||||
await Promise.all([p1, p2, p3, p4, p5, p6, p7, p8])
|
||||
|
||||
// Update server version in server settings
|
||||
if (this.previousVersion) {
|
||||
@@ -258,23 +270,6 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
updateUserStream(userId, streamId) {
|
||||
return this.usersDb.update((record) => record.id === userId, (user) => {
|
||||
user.stream = streamId
|
||||
return user
|
||||
}).then((results) => {
|
||||
Logger.debug(`[DB] Updated user ${results.updated}`)
|
||||
this.users = this.users.map(u => {
|
||||
if (u.id === userId) {
|
||||
u.stream = streamId
|
||||
}
|
||||
return u
|
||||
})
|
||||
}).catch((error) => {
|
||||
Logger.error(`[DB] Update user Failed ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
return this.updateEntity('settings', this.serverSettings)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user