mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-01 12:09:43 -05:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2793fe29b | ||
|
|
24b9ac6a68 | ||
|
|
9a5ed64fae | ||
|
|
c2af96e7cd | ||
|
|
104cadb0b3 | ||
|
|
6814adffcc | ||
|
|
20c11e381e | ||
|
|
b5952f16eb | ||
|
|
5b6878e5de | ||
|
|
89a25bcf39 | ||
|
|
d0cd512be8 | ||
|
|
3543dea0fb | ||
|
|
1949e25ccb | ||
|
|
b715ef3bfc | ||
|
|
954050df81 | ||
|
|
e4aa7f10fa | ||
|
|
2afd0e2acd | ||
|
|
0829237166 | ||
|
|
541975f038 | ||
|
|
01bf58ab97 | ||
|
|
d99b2c25e8 | ||
|
|
a31df5ff81 | ||
|
|
63e5cf2e60 | ||
|
|
7beca048e7 | ||
|
|
ec998dc1ac | ||
|
|
ddc54c8811 | ||
|
|
72e306935f | ||
|
|
96a7c7f4d1 | ||
|
|
9c65d655b8 | ||
|
|
b108f2241b | ||
|
|
9439acf300 | ||
|
|
d181e66d83 | ||
|
|
a87c3f2c77 | ||
|
|
2834f6077e | ||
|
|
918013ccb3 | ||
|
|
4c4672c6c1 | ||
|
|
b3991574c7 | ||
|
|
c881bcbe59 | ||
|
|
89aa4a8bdc | ||
|
|
c5a4f63670 | ||
|
|
1b97582975 | ||
|
|
9b7aacf3ea | ||
|
|
47b9ee557e | ||
|
|
e40e0bfa25 | ||
|
|
d56e3a3617 | ||
|
|
78fe6d47ba | ||
|
|
995cf51ae3 | ||
|
|
d838ff2f2e | ||
|
|
f2f07ff534 | ||
|
|
8cff68ca64 | ||
|
|
eb5331d34a | ||
|
|
f425185575 | ||
|
|
9fc352a5a4 | ||
|
|
e85ddc1aa1 | ||
|
|
b9be7510f8 | ||
|
|
f4497acd48 | ||
|
|
f73a0cce72 | ||
|
|
254ba1f089 | ||
|
|
0a179e4eed | ||
|
|
0ac63b2678 | ||
|
|
1d13d0a553 | ||
|
|
fc6ff016a7 | ||
|
|
e378b79fbc | ||
|
|
7e377297d7 | ||
|
|
00a02921dd | ||
|
|
b5d4c11f6f | ||
|
|
a0bc959850 | ||
|
|
a4b0f6c202 | ||
|
|
65cf928afe | ||
|
|
cf7fd315b6 | ||
|
|
d86a3b3dc2 | ||
|
|
e07e2cd359 | ||
|
|
8140d7021a | ||
|
|
bdbc5e3161 | ||
|
|
bb9013541b | ||
|
|
1668153acd | ||
|
|
aeba7674f8 | ||
|
|
5b0d105e21 | ||
|
|
feb54d0629 | ||
|
|
3284fe8f31 | ||
|
|
18cb394884 | ||
|
|
d0bce2949e | ||
|
|
a0e80772cd | ||
|
|
e44595521d | ||
|
|
fdf647eb32 | ||
|
|
71369bd2a0 | ||
|
|
36b1f43f4c | ||
|
|
a8bc1df3e7 | ||
|
|
a96869f547 | ||
|
|
77b030199e | ||
|
|
0e1c6c0ba7 | ||
|
|
c397422d3b | ||
|
|
15313826bf | ||
|
|
c6405b9013 | ||
|
|
d748d43efc | ||
|
|
d54edb93d6 | ||
|
|
b8ca6671fc | ||
|
|
cb7fb646ba | ||
|
|
aa82c8a253 | ||
|
|
aae92649b1 | ||
|
|
a9f5c64204 | ||
|
|
1392baf1eb | ||
|
|
0ec50bb570 | ||
|
|
b60473d7ae | ||
|
|
014fc45c15 | ||
|
|
4b4fb33d8f | ||
|
|
35e3458fb4 | ||
|
|
8f42153bee | ||
|
|
2f04d34bce | ||
|
|
09566c02ea | ||
|
|
d714ef37d9 | ||
|
|
fde07d26e5 | ||
|
|
9547824aaa | ||
|
|
5a01be1ee3 | ||
|
|
5dc4606657 | ||
|
|
2fd3238576 | ||
|
|
c1bcfe8304 | ||
|
|
a3642b204d | ||
|
|
8243da69f6 | ||
|
|
6d5987b2e0 | ||
|
|
a2fdc3e876 | ||
|
|
f92b66a469 | ||
|
|
c3d256c42b |
10
Dockerfile
10
Dockerfile
@@ -10,11 +10,15 @@ FROM sandreas/tone:v0.1.5 AS tone
|
||||
FROM node:16-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg
|
||||
ffmpeg \
|
||||
make \
|
||||
python3 \
|
||||
g++
|
||||
|
||||
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
||||
COPY --from=build /client/dist /client/dist
|
||||
@@ -23,6 +27,10 @@ COPY server server
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
RUN apk del make python3 g++
|
||||
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK \
|
||||
--interval=30s \
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link to="/">
|
||||
<h1 class="text-xl mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
|
||||
<h1 class="text-xl mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
|
||||
</nuxt-link>
|
||||
|
||||
<ui-libraries-dropdown class="mr-2" />
|
||||
@@ -149,9 +149,6 @@ export default {
|
||||
processingBatch() {
|
||||
return this.$store.state.processingBatch
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
isChromecastEnabled() {
|
||||
return this.$store.getters['getServerSetting']('chromecastEnabled')
|
||||
},
|
||||
@@ -306,13 +303,13 @@ export default {
|
||||
this.$axios
|
||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||
.then(() => {
|
||||
this.$toast.success('Batch update success!')
|
||||
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch update failed')
|
||||
this.$toast.error(this.$strings.ToastBatchUpdateFailed)
|
||||
console.error('Failed to batch update read/not read', error)
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
})
|
||||
|
||||
@@ -65,9 +65,6 @@ export default {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
@@ -171,7 +168,7 @@ export default {
|
||||
},
|
||||
async fetchCategories() {
|
||||
const categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- issues page remove all button -->
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" menu-width="110px" class="ml-2" @action="contextMenuAction" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
<!-- search page -->
|
||||
<template v-else-if="page === 'search'">
|
||||
|
||||
@@ -78,9 +78,6 @@ export default {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
libraryMediaType() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||
},
|
||||
@@ -318,10 +315,10 @@ export default {
|
||||
|
||||
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete`
|
||||
|
||||
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||
console.error('failed to fetch books', error)
|
||||
console.error('failed to fetch items', error)
|
||||
return null
|
||||
})
|
||||
|
||||
|
||||
@@ -116,9 +116,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<div v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -174,12 +179,6 @@ export default {
|
||||
dateFormat() {
|
||||
return this.store.state.serverSettings.dateFormat
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
},
|
||||
enableEReader() {
|
||||
return this.store.getters['getServerSetting']('enableEReader')
|
||||
},
|
||||
_libraryItem() {
|
||||
return this.libraryItem || {}
|
||||
},
|
||||
@@ -233,9 +232,11 @@ export default {
|
||||
return this.media.numTracks || 0 // toJSONMinified
|
||||
},
|
||||
numEpisodes() {
|
||||
if (!this.isPodcast) return 0
|
||||
return this.media.numEpisodes || 0
|
||||
},
|
||||
numEpisodesIncomplete() {
|
||||
return this._libraryItem.numEpisodesIncomplete || 0
|
||||
},
|
||||
processingBatch() {
|
||||
return this.store.state.processingBatch
|
||||
},
|
||||
@@ -367,13 +368,13 @@ export default {
|
||||
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
||||
},
|
||||
showReadButton() {
|
||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||
return !this.isSelectionMode && this.ebookFormat
|
||||
},
|
||||
isMissing() {
|
||||
return this._libraryItem.isMissing
|
||||
@@ -686,7 +687,6 @@ export default {
|
||||
.$patch(apiEndpoint, updatePayload)
|
||||
.then(() => {
|
||||
this.processing = false
|
||||
toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
@@ -763,6 +763,8 @@ export default {
|
||||
this.store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
removeSeriesFromContinueListening() {
|
||||
if (!this.series) return
|
||||
|
||||
const axios = this.$axios || this.$nuxt.$axios
|
||||
this.processing = true
|
||||
axios
|
||||
@@ -888,7 +890,7 @@ export default {
|
||||
var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
|
||||
var el = instance.$el
|
||||
|
||||
var elHeight = this.moreMenuItems.length * 28 + 2
|
||||
var elHeight = this.moreMenuItems.length * 28 + 10
|
||||
var elWidth = 130
|
||||
|
||||
var bottomOfIcon = wrapperBox.top + wrapperBox.height
|
||||
@@ -921,7 +923,7 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!libraryItem) return
|
||||
this.store.commit('showEReader', libraryItem)
|
||||
this.store.commit('showEReader', { libraryItem, keepProgress: true })
|
||||
},
|
||||
selectBtnClick(evt) {
|
||||
if (this.processingBatch) return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publishedYear" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -20,15 +20,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publisher" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ publisher }}
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
</div>
|
||||
<div class="capitalize">
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="genres.length">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="tags.length">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs">{{ selectedText }}</span>
|
||||
</span>
|
||||
@@ -14,12 +14,17 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
</div>
|
||||
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
@@ -9,7 +9,7 @@
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -17,23 +17,27 @@
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate text-sm">{{ 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 text-2xl">arrow_right</span>
|
||||
</div>
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||
<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>
|
||||
<span class="font-normal block truncate">Back</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
@@ -41,16 +45,15 @@
|
||||
<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'))">
|
||||
<div class="flex items-center">
|
||||
<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">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedSublistOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||
</div>
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
@@ -72,9 +75,8 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
showMenu(newVal) {
|
||||
if (!newVal) {
|
||||
if (this.sublist && !this.selectedItemSublist) this.sublist = null
|
||||
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
|
||||
if (newVal) {
|
||||
this.sublist = this.selectedItemSublist
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -122,6 +124,11 @@ export default {
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
@@ -165,6 +172,11 @@ export default {
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
@@ -186,9 +198,9 @@ export default {
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelEbook,
|
||||
value: 'ebook',
|
||||
sublist: false
|
||||
text: this.$strings.LabelEbooks,
|
||||
value: 'ebooks',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAbridged,
|
||||
@@ -260,21 +272,25 @@ export default {
|
||||
return this.bookItems
|
||||
},
|
||||
selectedItemSublist() {
|
||||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||
return this.selected?.includes('.') ? this.selected.split('.')[0] : null
|
||||
},
|
||||
selectedText() {
|
||||
if (!this.selected) return ''
|
||||
var parts = this.selected.split('.')
|
||||
var filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||
var filterValue = null
|
||||
const parts = this.selected.split('.')
|
||||
const filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||
let filterValue = null
|
||||
if (parts.length > 1) {
|
||||
var decoded = this.$decode(parts[1])
|
||||
if (decoded.startsWith('aut_')) {
|
||||
var author = this.authors.find((au) => au.id == decoded)
|
||||
const decoded = this.$decode(parts[1])
|
||||
if (parts[0] === 'authors') {
|
||||
const author = this.authors.find((au) => au.id == decoded)
|
||||
if (author) filterValue = author.name
|
||||
} else if (decoded.startsWith('ser_')) {
|
||||
var series = this.series.find((se) => se.id == decoded)
|
||||
if (series) filterValue = series.name
|
||||
} else if (parts[0] === 'series') {
|
||||
if (decoded === 'no-series') {
|
||||
filterValue = this.$strings.MessageNoSeries
|
||||
} else {
|
||||
const series = this.series.find((se) => se.id == decoded)
|
||||
if (series) filterValue = series.name
|
||||
}
|
||||
} else {
|
||||
filterValue = decoded
|
||||
}
|
||||
@@ -307,6 +323,9 @@ export default {
|
||||
languages() {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
publishers() {
|
||||
return this.filterData.publishers || []
|
||||
},
|
||||
progress() {
|
||||
return [
|
||||
{
|
||||
@@ -339,6 +358,18 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
ebooks() {
|
||||
return [
|
||||
{
|
||||
id: 'ebook',
|
||||
name: this.$strings.LabelHasEbook
|
||||
},
|
||||
{
|
||||
id: 'supplementary',
|
||||
name: this.$strings.LabelHasSupplementaryEbook
|
||||
}
|
||||
]
|
||||
},
|
||||
missing() {
|
||||
return [
|
||||
{
|
||||
@@ -396,7 +427,7 @@ export default {
|
||||
]
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
const sublistItems = (this[this.sublist] || []).map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
text: item,
|
||||
@@ -409,6 +440,13 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
if (this.sublist === 'series') {
|
||||
sublistItems.unshift({
|
||||
text: this.$strings.MessageNoSeries,
|
||||
value: this.$encode('no-series')
|
||||
})
|
||||
}
|
||||
return sublistItems
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
@@ -433,7 +471,7 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
var val = option.value
|
||||
const val = option.value
|
||||
if (this.selected === val) {
|
||||
this.showMenu = false
|
||||
return
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
</div>
|
||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
</div>
|
||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<div class="w-full p-8">
|
||||
<div class="flex py-2">
|
||||
<div class="w-1/2 px-2">
|
||||
@@ -103,7 +103,6 @@
|
||||
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -353,7 +352,8 @@ export default {
|
||||
accessAllTags: true,
|
||||
selectedTagsNotAccessible: false
|
||||
},
|
||||
librariesAccessible: []
|
||||
librariesAccessible: [],
|
||||
itemTagsSelected: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,99 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="audiofile-data-modal" :width="700" :height="'unset'">
|
||||
<div v-if="audioFile" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
||||
<p class="text-base text-gray-200">{{ metadata.filename }}</p>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
|
||||
<ui-text-input-with-label :value="metadata.path" readonly :label="$strings.LabelPath" class="mb-4 text-sm" />
|
||||
|
||||
<div class="flex flex-col sm:flex-row text-sm">
|
||||
<div class="w-full sm:w-1/2">
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelSize }}
|
||||
</p>
|
||||
<p>{{ $bytesPretty(metadata.size) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelDuration }}
|
||||
</p>
|
||||
<p>{{ $secondsToTimestamp(audioFile.duration) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">{{ $strings.LabelFormat }}</p>
|
||||
<p>{{ audioFile.format }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelChapters }}
|
||||
</p>
|
||||
<p>{{ audioFile.chapters?.length || 0 }}</p>
|
||||
</div>
|
||||
<div v-if="audioFile.embeddedCoverArt" class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelEmbeddedCover }}
|
||||
</p>
|
||||
<p>{{ audioFile.embeddedCoverArt || '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/2">
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelCodec }}
|
||||
</p>
|
||||
<p>{{ audioFile.codec }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelChannels }}
|
||||
</p>
|
||||
<p>{{ audioFile.channels }} ({{ audioFile.channelLayout }})</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelBitrate }}
|
||||
</p>
|
||||
<p>{{ $bytesPretty(audioFile.bitRate || 0, 0) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">{{ $strings.LabelTimeBase }}</p>
|
||||
<p>{{ audioFile.timeBase }}</p>
|
||||
</div>
|
||||
<div v-if="audioFile.language" class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelLanguage }}
|
||||
</p>
|
||||
<p>{{ audioFile.language || '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
||||
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
|
||||
<p class="font-bold mb-2">{{ $strings.LabelMetaTags }}</p>
|
||||
<template v-if="!ffprobeData">
|
||||
<ui-text-input-with-label :value="metadata.path" readonly :label="$strings.LabelPath" class="mb-4 text-sm" />
|
||||
|
||||
<div v-for="(value, key) in metaTags" :key="key" class="flex mb-1 text-sm">
|
||||
<p class="w-32 min-w-32 text-black-50 mb-1">
|
||||
{{ key.replace('tag', '') }}
|
||||
</p>
|
||||
<p>{{ value }}</p>
|
||||
<div class="flex flex-col sm:flex-row text-sm">
|
||||
<div class="w-full sm:w-1/2">
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelSize }}
|
||||
</p>
|
||||
<p>{{ $bytesPretty(metadata.size) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelDuration }}
|
||||
</p>
|
||||
<p>{{ $secondsToTimestamp(audioFile.duration) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">{{ $strings.LabelFormat }}</p>
|
||||
<p>{{ audioFile.format }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelChapters }}
|
||||
</p>
|
||||
<p>{{ audioFile.chapters?.length || 0 }}</p>
|
||||
</div>
|
||||
<div v-if="audioFile.embeddedCoverArt" class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelEmbeddedCover }}
|
||||
</p>
|
||||
<p>{{ audioFile.embeddedCoverArt || '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/2">
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelCodec }}
|
||||
</p>
|
||||
<p>{{ audioFile.codec }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelChannels }}
|
||||
</p>
|
||||
<p>{{ audioFile.channels }} ({{ audioFile.channelLayout }})</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelBitrate }}
|
||||
</p>
|
||||
<p>{{ $bytesPretty(audioFile.bitRate || 0, 0) }}</p>
|
||||
</div>
|
||||
<div class="flex mb-1">
|
||||
<p class="w-32 text-black-50">{{ $strings.LabelTimeBase }}</p>
|
||||
<p>{{ audioFile.timeBase }}</p>
|
||||
</div>
|
||||
<div v-if="audioFile.language" class="flex mb-1">
|
||||
<p class="w-32 text-black-50">
|
||||
{{ $strings.LabelLanguage }}
|
||||
</p>
|
||||
<p>{{ audioFile.language || '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
|
||||
<p class="font-bold mb-2">{{ $strings.LabelMetaTags }}</p>
|
||||
|
||||
<div v-for="(value, key) in metaTags" :key="key" class="flex mb-1 text-sm">
|
||||
<p class="w-32 min-w-32 text-black-50 mb-1">
|
||||
{{ key.replace('tag', '') }}
|
||||
</p>
|
||||
<p>{{ value }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="w-full">
|
||||
<div class="relative">
|
||||
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
|
||||
|
||||
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
|
||||
<span class="material-icons">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -91,10 +106,24 @@ export default {
|
||||
audioFile: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
libraryItemId: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
probingFile: false,
|
||||
ffprobeData: null,
|
||||
copiedToClipboard: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.ffprobeData = null
|
||||
this.copiedToClipboard = false
|
||||
this.probingFile = false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
@@ -110,9 +139,36 @@ export default {
|
||||
},
|
||||
metaTags() {
|
||||
return this.audioFile?.metaTags || {}
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
prettyFfprobeData() {
|
||||
if (!this.ffprobeData) return ''
|
||||
return JSON.stringify(this.ffprobeData, null, 2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFFProbeData() {
|
||||
this.probingFile = true
|
||||
this.$axios
|
||||
.$get(`/api/items/${this.libraryItemId}/ffprobe/${this.audioFile.ino}`)
|
||||
.then((data) => {
|
||||
console.log('Got ffprobe data', data)
|
||||
this.ffprobeData = data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get ffprobe data', error)
|
||||
this.$toast.error('FFProbe failed')
|
||||
})
|
||||
.finally(() => {
|
||||
this.probingFile = false
|
||||
})
|
||||
},
|
||||
async copyFfprobeData() {
|
||||
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clickedOption(action) {
|
||||
this.$emit('action', action)
|
||||
this.$emit('action', { action })
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
||||
<p class="text-lg md:text-2xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
||||
@@ -50,19 +50,19 @@
|
||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelItem }}</p>
|
||||
<div v-if="_session.libraryId" class="flex items-center -mx-1 mb-1">
|
||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelLibrary }} Id</div>
|
||||
<div class="px-1">
|
||||
<div class="px-1 text-xs">
|
||||
{{ _session.libraryId }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center -mx-1 mb-1">
|
||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelLibraryItem }} Id</div>
|
||||
<div class="px-1">
|
||||
<div class="px-1 text-xs">
|
||||
{{ _session.libraryItemId }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="_session.episodeId" class="flex items-center -mx-1 mb-1">
|
||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelEpisode }} Id</div>
|
||||
<div class="px-1">
|
||||
<div class="px-1 text-xs">
|
||||
{{ _session.episodeId }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
<div class="w-full md:w-1/3">
|
||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
|
||||
<p class="mb-1">{{ _session.userId }}</p>
|
||||
<p class="mb-1 text-xs">{{ _session.userId }}</p>
|
||||
|
||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
|
||||
<p class="mb-1">{{ playMethodName }}</p>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
|
||||
<div class="absolute top-3 right-3 landscape:top-2 landscape:right-2 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 h-8 w-8 landscape:h-8 landscape:w-8 md:portrait:h-12 md:portrait:w-12 lg:w-12 lg:h-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="clickClose">
|
||||
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||
<span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||
</div>
|
||||
</button>
|
||||
<slot name="outer" />
|
||||
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<slot />
|
||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
<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">
|
||||
<template v-if="!showImageUploader">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full flex justify-center mb-2 md:w-auto md:mb-0 md:block">
|
||||
<covers-collection-cover :book-items="books" :width="200" :height="100 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<!-- <ui-btn type="button" @click="showImageUploader = true">Upload</ui-btn> -->
|
||||
</div>
|
||||
<div class="flex-grow px-4">
|
||||
<ui-text-input-with-label v-model="newCollectionName" :label="$strings.LabelName" class="mb-2" />
|
||||
@@ -41,7 +40,6 @@
|
||||
<ui-btn color="success">Upload</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <modals-upload-image-modal v-model="showUploadImageModal" entity="collection" :entity-id="collection.id" /> -->
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
@@ -127,9 +127,6 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
@@ -154,7 +151,6 @@ export default {
|
||||
availableTabs() {
|
||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||
return this.tabs.filter((tab) => {
|
||||
if (tab.experimental && !this.showExperimentalFeatures) return false
|
||||
if (tab.mediaType && this.mediaType !== tab.mediaType) return false
|
||||
if (tab.admin && !this.userIsAdminOrUp) return false
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex flex-wrap mb-4">
|
||||
<div class="relative">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<!-- book cover overlay -->
|
||||
<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">
|
||||
@@ -139,16 +139,19 @@ export default {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
return this.libraryItem?.id || null
|
||||
},
|
||||
libraryItemUpdatedAt() {
|
||||
return this.libraryItem?.updatedAt || null
|
||||
},
|
||||
mediaType() {
|
||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
||||
return this.libraryItem?.mediaType || null
|
||||
},
|
||||
isPodcast() {
|
||||
return this.mediaType == 'podcast'
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
return this.libraryItem?.media || {}
|
||||
},
|
||||
coverPath() {
|
||||
return this.media.coverPath
|
||||
@@ -157,7 +160,7 @@ export default {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
libraryFiles() {
|
||||
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
||||
return this.libraryItem?.libraryFiles || []
|
||||
},
|
||||
userCanUpload() {
|
||||
return this.$store.getters['user/getUserCanUpload']
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Split to mp3 -->
|
||||
<div v-if="showMp3Split && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
|
||||
<!-- <div v-if="showMp3Split" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
|
||||
@@ -31,7 +31,7 @@
|
||||
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Embed Metadata -->
|
||||
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
||||
@@ -79,9 +79,6 @@ export default {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem?.id || null
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||
<div class="flex items-center py-2">
|
||||
<div class="flex items-center py-3">
|
||||
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||
<p class="pl-4 text-base">
|
||||
@@ -17,18 +17,38 @@
|
||||
</div>
|
||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
||||
</div>
|
||||
<div v-if="mediaType == 'book'" class="py-3">
|
||||
<div v-if="isBookLibrary" class="flex items-center py-3">
|
||||
<ui-toggle-switch v-model="audiobooksOnly" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsAudiobooksOnly }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mediaType == 'book'" class="py-3">
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="hideSingleBookSeries" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -47,7 +67,9 @@ export default {
|
||||
useSquareBookCovers: false,
|
||||
disableWatcher: false,
|
||||
skipMatchingMediaWithAsin: false,
|
||||
skipMatchingMediaWithIsbn: false
|
||||
skipMatchingMediaWithIsbn: false,
|
||||
audiobooksOnly: false,
|
||||
hideSingleBookSeries: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -60,6 +82,9 @@ export default {
|
||||
mediaType() {
|
||||
return this.library.mediaType
|
||||
},
|
||||
isBookLibrary() {
|
||||
return this.mediaType === 'book'
|
||||
},
|
||||
providers() {
|
||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||
return this.$store.state.scanners.providers
|
||||
@@ -72,7 +97,9 @@ export default {
|
||||
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||
disableWatcher: !!this.disableWatcher,
|
||||
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn
|
||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
||||
audiobooksOnly: !!this.audiobooksOnly,
|
||||
hideSingleBookSeries: !!this.hideSingleBookSeries
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -84,6 +111,8 @@ export default {
|
||||
this.disableWatcher = !!this.librarySettings.disableWatcher
|
||||
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
||||
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
||||
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
||||
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-4">
|
||||
<ui-checkbox v-if="!allDownloaded" v-model="selectAll" @input="toggleSelectAll" label="Select all episodes" small checkbox-bg="primary" border-color="gray-600" class="mx-8" />
|
||||
<ui-checkbox v-if="!allDownloaded" v-model="selectAll" @input="toggleSelectAll" :label="selectAllLabel" small checkbox-bg="primary" border-color="gray-600" class="mx-8" />
|
||||
<ui-btn v-if="!allDownloaded" :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
|
||||
<p v-else class="text-success text-base px-2 py-4">All episodes are downloaded</p>
|
||||
</div>
|
||||
@@ -99,46 +99,82 @@ export default {
|
||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||
},
|
||||
buttonText() {
|
||||
if (!this.episodesSelected.length) return 'No Episodes Selected'
|
||||
return `Download ${this.episodesSelected.length} Episode${this.episodesSelected.length > 1 ? 's' : ''}`
|
||||
if (!this.episodesSelected.length) return this.$strings.LabelNoEpisodesSelected
|
||||
if (this.episodesSelected.length === 1) return `${this.$strings.LabelDownload} ${this.$strings.LabelEpisode.toLowerCase()}`
|
||||
return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length])
|
||||
},
|
||||
itemEpisodes() {
|
||||
if (!this.libraryItem) return []
|
||||
return this.libraryItem.media.episodes || []
|
||||
},
|
||||
itemEpisodeMap() {
|
||||
var map = {}
|
||||
const map = {}
|
||||
this.itemEpisodes.forEach((item) => {
|
||||
if (item.enclosure) map[item.enclosure.url.split('?')[0]] = true
|
||||
if (item.enclosure) {
|
||||
const cleanUrl = this.getCleanEpisodeUrl(item.enclosure.url)
|
||||
map[cleanUrl] = true
|
||||
}
|
||||
})
|
||||
return map
|
||||
},
|
||||
episodesList() {
|
||||
return this.episodesCleaned.filter((episode) => {
|
||||
if (!this.searchText) return true
|
||||
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||
return episode.title?.toLowerCase().includes(this.searchText) || episode.subtitle?.toLowerCase().includes(this.searchText)
|
||||
})
|
||||
},
|
||||
selectAllLabel() {
|
||||
if (this.episodesList.length === this.episodesCleaned.length) {
|
||||
return this.$strings.LabelSelectAllEpisodes
|
||||
}
|
||||
const episodesNotDownloaded = this.episodesList.filter((ep) => !this.itemEpisodeMap[ep.cleanUrl]).length
|
||||
return this.$getString('LabelSelectEpisodesShowing', [episodesNotDownloaded])
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* RSS feed episode url is used for matching with existing downloaded episodes.
|
||||
* Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests.
|
||||
* These need to be removed in order to detect the same episode each time the feed is pulled.
|
||||
*
|
||||
* An RSS feed may include an `id` in the query string. In these cases we want to leave the `id`.
|
||||
* @see https://github.com/advplyr/audiobookshelf/issues/1896
|
||||
*
|
||||
* @param {string} url - rss feed episode url
|
||||
* @returns {string} rss feed episode url without dynamic query strings
|
||||
*/
|
||||
getCleanEpisodeUrl(url) {
|
||||
let queryString = url.split('?')[1]
|
||||
if (!queryString) return url
|
||||
|
||||
const searchParams = new URLSearchParams(queryString)
|
||||
for (const p of Array.from(searchParams.keys())) {
|
||||
if (p !== 'id') searchParams.delete(p)
|
||||
}
|
||||
|
||||
if (!searchParams.toString()) return url
|
||||
return `${url}?${searchParams.toString()}`
|
||||
},
|
||||
inputUpdate() {
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
if (!this.search || !this.search.trim()) {
|
||||
if (!this.search?.trim()) {
|
||||
this.searchText = ''
|
||||
this.checkSetIsSelectedAll()
|
||||
return
|
||||
}
|
||||
this.searchText = this.search.toLowerCase().trim()
|
||||
this.checkSetIsSelectedAll()
|
||||
}, 500)
|
||||
},
|
||||
toggleSelectAll(val) {
|
||||
for (const episode of this.episodesCleaned) {
|
||||
for (const episode of this.episodesList) {
|
||||
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
|
||||
else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
|
||||
}
|
||||
},
|
||||
checkSetIsSelectedAll() {
|
||||
for (const episode of this.episodesCleaned) {
|
||||
for (const episode of this.episodesList) {
|
||||
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
|
||||
this.selectAll = false
|
||||
return
|
||||
@@ -147,19 +183,19 @@ export default {
|
||||
this.selectAll = true
|
||||
},
|
||||
toggleSelectEpisode(episode) {
|
||||
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
||||
if (this.itemEpisodeMap[episode.cleanUrl]) return
|
||||
this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
|
||||
this.checkSetIsSelectedAll()
|
||||
},
|
||||
submit() {
|
||||
var episodesToDownload = []
|
||||
let episodesToDownload = []
|
||||
if (this.episodesSelected.length) {
|
||||
episodesToDownload = this.episodesSelected.map((cleanUrl) => this.episodesCleaned.find((ep) => ep.cleanUrl == cleanUrl))
|
||||
}
|
||||
|
||||
var payloadSize = JSON.stringify(episodesToDownload).length
|
||||
var sizeInMb = payloadSize / 1024 / 1024
|
||||
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||
const payloadSize = JSON.stringify(episodesToDownload).length
|
||||
const sizeInMb = payloadSize / 1024 / 1024
|
||||
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||
console.log('Request size', sizeInMb)
|
||||
if (sizeInMb > 4.99) {
|
||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
||||
@@ -174,10 +210,9 @@ export default {
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to download episodes'
|
||||
console.error('Failed to download episodes', error)
|
||||
this.processing = false
|
||||
this.$toast.error(errorMsg)
|
||||
this.$toast.error(error.response?.data || 'Failed to download episodes')
|
||||
|
||||
this.selectedEpisodes = {}
|
||||
this.selectAll = false
|
||||
@@ -189,7 +224,7 @@ export default {
|
||||
.map((_ep) => {
|
||||
return {
|
||||
..._ep,
|
||||
cleanUrl: _ep.enclosure.url.split('?')[0]
|
||||
cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url)
|
||||
}
|
||||
})
|
||||
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
||||
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
||||
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400" :style="{ width: pageMenuWidth + 'px' }">
|
||||
<div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index + 1)">
|
||||
<p class="text-sm truncate">{{ file }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,23 +14,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
||||
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-32' : 'left-20'">
|
||||
<span class="material-icons text-xl">download</span>
|
||||
</a>
|
||||
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
|
||||
<span class="material-icons text-xl">more</span>
|
||||
</div>
|
||||
<div class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
||||
<div v-if="numPages" class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
|
||||
<span class="material-icons text-xl">menu</span>
|
||||
</div>
|
||||
<div class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
||||
<div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden w-full h-full relative">
|
||||
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||
<div class="flex items-center justify-center h-full w-1/2">
|
||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
||||
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
||||
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
|
||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
|
||||
</div>
|
||||
@@ -61,7 +64,9 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
playerOpen: Boolean
|
||||
playerOpen: Boolean,
|
||||
keepProgress: Boolean,
|
||||
fileId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -71,6 +76,7 @@ export default {
|
||||
mainImg: null,
|
||||
page: 0,
|
||||
numPages: 0,
|
||||
pageMenuWidth: 256,
|
||||
showPageMenu: false,
|
||||
showInfoMenu: false,
|
||||
loadTimeout: null,
|
||||
@@ -94,19 +100,72 @@ export default {
|
||||
return this.libraryItem?.id
|
||||
},
|
||||
ebookUrl() {
|
||||
if (this.fileId) {
|
||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
},
|
||||
comicMetadataKeys() {
|
||||
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
||||
},
|
||||
canGoNext() {
|
||||
return this.page < this.numPages - 1
|
||||
return this.page < this.numPages
|
||||
},
|
||||
canGoPrev() {
|
||||
return this.page > 0
|
||||
return this.page > 1
|
||||
},
|
||||
userMediaProgress() {
|
||||
if (!this.libraryItemId) return
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
savedPage() {
|
||||
if (!this.keepProgress) return 0
|
||||
|
||||
// Validate ebookLocation is a number
|
||||
if (!this.userMediaProgress?.ebookLocation || isNaN(this.userMediaProgress.ebookLocation)) return 0
|
||||
return Number(this.userMediaProgress.ebookLocation)
|
||||
},
|
||||
cleanedPageNames() {
|
||||
return (
|
||||
this.pages?.map((p) => {
|
||||
if (p.length > 50) {
|
||||
let firstHalf = p.slice(0, 22)
|
||||
let lastHalf = p.slice(p.length - 23)
|
||||
return `${firstHalf} ... ${lastHalf}`
|
||||
}
|
||||
return p
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickShowPageMenu() {
|
||||
this.showInfoMenu = false
|
||||
this.showPageMenu = !this.showPageMenu
|
||||
},
|
||||
clickShowInfoMenu() {
|
||||
this.showPageMenu = false
|
||||
this.showInfoMenu = !this.showInfoMenu
|
||||
},
|
||||
updateProgress() {
|
||||
if (!this.keepProgress) return
|
||||
|
||||
if (!this.numPages) {
|
||||
console.error('Num pages not loaded')
|
||||
return
|
||||
}
|
||||
if (this.savedPage === this.page) {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
ebookLocation: this.page,
|
||||
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||
}
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||
console.error('ComicReader.updateProgress failed:', error)
|
||||
})
|
||||
},
|
||||
clickOutside() {
|
||||
if (this.showPageMenu) this.showPageMenu = false
|
||||
if (this.showInfoMenu) this.showInfoMenu = false
|
||||
@@ -119,12 +178,15 @@ export default {
|
||||
if (!this.canGoPrev) return
|
||||
this.setPage(this.page - 1)
|
||||
},
|
||||
setPage(index) {
|
||||
if (index < 0 || index > this.numPages - 1) {
|
||||
setPage(page) {
|
||||
if (page <= 0 || page > this.numPages) {
|
||||
return
|
||||
}
|
||||
var filename = this.pages[index]
|
||||
this.page = index
|
||||
this.showPageMenu = false
|
||||
this.showInfoMenu = false
|
||||
const filename = this.pages[page - 1]
|
||||
this.page = page
|
||||
this.updateProgress()
|
||||
return this.extractFile(filename)
|
||||
},
|
||||
setLoadTimeout() {
|
||||
@@ -174,9 +236,28 @@ export default {
|
||||
|
||||
this.numPages = this.pages.length
|
||||
|
||||
// Calculate page menu size
|
||||
const largestFilename = this.cleanedPageNames
|
||||
.map((p) => p)
|
||||
.sort((a, b) => a.length - b.length)
|
||||
.pop()
|
||||
const pEl = document.createElement('p')
|
||||
pEl.innerText = largestFilename
|
||||
pEl.style.fontSize = '0.875rem'
|
||||
pEl.style.opacity = 0
|
||||
pEl.style.position = 'absolute'
|
||||
document.body.appendChild(pEl)
|
||||
const textWidth = pEl.getBoundingClientRect()?.width
|
||||
if (textWidth) {
|
||||
this.pageMenuWidth = textWidth + (16 + 5 + 2 + 5)
|
||||
}
|
||||
pEl.remove()
|
||||
|
||||
if (this.pages.length) {
|
||||
this.loading = false
|
||||
await this.setPage(0)
|
||||
|
||||
const startPage = this.savedPage > 0 && this.savedPage <= this.numPages ? this.savedPage : 1
|
||||
await this.setPage(startPage)
|
||||
this.loadedFirstPage = true
|
||||
} else {
|
||||
this.$toast.error('Unable to extract pages')
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div id="epub-reader" class="h-full w-full">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||
</div>
|
||||
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
|
||||
<span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||
</button>
|
||||
<div id="frame" class="w-full" style="height: 80%">
|
||||
<div id="viewer"></div>
|
||||
</div>
|
||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center justify-center overflow-x-hidden">
|
||||
<span v-if="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||
</div>
|
||||
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
|
||||
<span v-if="hasNext" class="material-icons text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -28,7 +28,9 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
playerOpen: Boolean
|
||||
playerOpen: Boolean,
|
||||
keepProgress: Boolean,
|
||||
fileId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -37,7 +39,13 @@ export default {
|
||||
/** @type {ePub.Book} */
|
||||
book: null,
|
||||
/** @type {ePub.Rendition} */
|
||||
rendition: null
|
||||
rendition: null,
|
||||
ereaderSettings: {
|
||||
theme: 'dark',
|
||||
fontScale: 100,
|
||||
lineSpacing: 115,
|
||||
spread: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -61,12 +69,19 @@ export default {
|
||||
},
|
||||
/** @returns {Array<ePub.NavItem>} */
|
||||
chapters() {
|
||||
return this.book ? this.book.navigation.toc : []
|
||||
return this.book?.navigation?.toc || []
|
||||
},
|
||||
userMediaProgress() {
|
||||
if (!this.libraryItemId) return
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
savedEbookLocation() {
|
||||
if (!this.keepProgress) return null
|
||||
if (!this.userMediaProgress?.ebookLocation) return null
|
||||
// Validate ebookLocation is an epubcfi
|
||||
if (!String(this.userMediaProgress.ebookLocation).startsWith('epubcfi')) return null
|
||||
return this.userMediaProgress.ebookLocation
|
||||
},
|
||||
localStorageLocationsKey() {
|
||||
return `ebookLocations-${this.libraryItemId}`
|
||||
},
|
||||
@@ -78,11 +93,45 @@ export default {
|
||||
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
||||
return this.windowHeight - 164
|
||||
},
|
||||
epubUrl() {
|
||||
ebookUrl() {
|
||||
if (this.fileId) {
|
||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
},
|
||||
themeRules() {
|
||||
const isDark = this.ereaderSettings.theme === 'dark'
|
||||
const fontColor = isDark ? '#fff' : '#000'
|
||||
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)'
|
||||
|
||||
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
||||
|
||||
const fontScale = this.ereaderSettings.fontScale / 100
|
||||
|
||||
return {
|
||||
'*': {
|
||||
color: `${fontColor}!important`,
|
||||
'background-color': `${backgroundColor}!important`,
|
||||
'line-height': lineSpacing * fontScale + 'rem!important'
|
||||
},
|
||||
a: {
|
||||
color: `${fontColor}!important`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSettings(settings) {
|
||||
this.ereaderSettings = settings
|
||||
|
||||
if (!this.rendition) return
|
||||
|
||||
this.applyTheme()
|
||||
|
||||
const fontScale = settings.fontScale || 100
|
||||
this.rendition.themes.fontSize(`${fontScale}%`)
|
||||
this.rendition.spread(settings.spread || 'auto')
|
||||
},
|
||||
prev() {
|
||||
return this.rendition?.prev()
|
||||
},
|
||||
@@ -106,6 +155,7 @@ export default {
|
||||
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
||||
*/
|
||||
updateProgress(payload) {
|
||||
if (!this.keepProgress) return
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||
console.error('EpubReader.updateProgress failed:', error)
|
||||
})
|
||||
@@ -197,7 +247,7 @@ export default {
|
||||
},
|
||||
/** @param {string} location - CFI of the new location */
|
||||
relocated(location) {
|
||||
if (this.userMediaProgress?.ebookLocation === location.start.cfi) {
|
||||
if (this.savedEbookLocation === location.start.cfi) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,7 +267,7 @@ export default {
|
||||
const reader = this
|
||||
|
||||
/** @type {ePub.Book} */
|
||||
reader.book = new ePub(reader.epubUrl, {
|
||||
reader.book = new ePub(reader.ebookUrl, {
|
||||
width: this.readerWidth,
|
||||
height: this.readerHeight - 50,
|
||||
openAs: 'epub',
|
||||
@@ -229,35 +279,30 @@ export default {
|
||||
/** @type {ePub.Rendition} */
|
||||
reader.rendition = reader.book.renderTo('viewer', {
|
||||
width: this.readerWidth,
|
||||
height: this.readerHeight * 0.8
|
||||
height: this.readerHeight * 0.8,
|
||||
spread: 'auto',
|
||||
snap: true,
|
||||
manager: 'continuous',
|
||||
flow: 'paginated'
|
||||
})
|
||||
|
||||
// load saved progress
|
||||
reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start)
|
||||
reader.rendition.display(this.savedEbookLocation || reader.book.locations.start)
|
||||
|
||||
// load style
|
||||
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' } })
|
||||
reader.rendition.on('rendered', () => {
|
||||
this.applyTheme()
|
||||
})
|
||||
|
||||
reader.book.ready.then(() => {
|
||||
// set up event listeners
|
||||
reader.rendition.on('relocated', reader.relocated)
|
||||
reader.rendition.on('keydown', reader.keyUp)
|
||||
|
||||
let touchStart = 0
|
||||
let touchEnd = 0
|
||||
reader.rendition.on('touchstart', (event) => {
|
||||
touchStart = event.changedTouches[0].screenX
|
||||
this.$emit('touchstart', event)
|
||||
})
|
||||
|
||||
reader.rendition.on('touchend', (event) => {
|
||||
touchEnd = event.changedTouches[0].screenX
|
||||
const touchDistanceX = Math.abs(touchEnd - touchStart)
|
||||
if (touchStart < touchEnd && touchDistanceX > 120) {
|
||||
this.next()
|
||||
}
|
||||
if (touchStart > touchEnd && touchDistanceX > 120) {
|
||||
this.prev()
|
||||
}
|
||||
this.$emit('touchend', event)
|
||||
})
|
||||
|
||||
// load ebook cfi locations
|
||||
@@ -275,6 +320,12 @@ export default {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.windowHeight = window.innerHeight
|
||||
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
||||
},
|
||||
applyTheme() {
|
||||
if (!this.rendition) return
|
||||
this.rendition.getContents().forEach((c) => {
|
||||
c.addStylesheetRules(this.themeRules)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -19,7 +19,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
playerOpen: Boolean
|
||||
playerOpen: Boolean,
|
||||
fileId: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -32,6 +33,9 @@ export default {
|
||||
return this.libraryItem?.id
|
||||
},
|
||||
ebookUrl() {
|
||||
if (this.fileId) {
|
||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center">
|
||||
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||
</div>
|
||||
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center">
|
||||
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
|
||||
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
|
||||
</div>
|
||||
@@ -45,7 +45,9 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
playerOpen: Boolean
|
||||
playerOpen: Boolean,
|
||||
keepProgress: Boolean,
|
||||
fileId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -95,11 +97,21 @@ export default {
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
savedPage() {
|
||||
return Number(this.userMediaProgress?.ebookLocation || 0)
|
||||
if (!this.keepProgress) return 0
|
||||
|
||||
// Validate ebookLocation is a number
|
||||
if (!this.userMediaProgress?.ebookLocation || isNaN(this.userMediaProgress.ebookLocation)) return 0
|
||||
return Number(this.userMediaProgress.ebookLocation)
|
||||
},
|
||||
ebookUrl() {
|
||||
if (this.fileId) {
|
||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
},
|
||||
pdfDocInitParams() {
|
||||
return {
|
||||
url: `/api/items/${this.libraryItemId}/ebook`,
|
||||
url: this.ebookUrl,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
@@ -114,6 +126,7 @@ export default {
|
||||
this.scale -= 0.1
|
||||
},
|
||||
updateProgress() {
|
||||
if (!this.keepProgress) return
|
||||
if (!this.numPages) {
|
||||
console.error('Num pages not loaded')
|
||||
return
|
||||
@@ -128,7 +141,7 @@ export default {
|
||||
})
|
||||
},
|
||||
loadedEvt() {
|
||||
if (this.savedPage && this.savedPage > 0 && this.savedPage <= this.numPages) {
|
||||
if (this.savedPage > 0 && this.savedPage <= this.numPages) {
|
||||
this.page = this.savedPage
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
<template>
|
||||
<div v-if="show" id="reader" class="absolute top-0 left-0 w-full z-60 bg-primary text-white" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||
<div class="absolute top-4 left-4 z-20">
|
||||
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
||||
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||
<div class="absolute top-4 left-4 z-20 flex items-center">
|
||||
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
|
||||
<span class="material-icons text-2xl">menu</span>
|
||||
</button>
|
||||
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100">
|
||||
<span class="material-icons text-1.5xl">settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-4 left-1/2 transform -translate-x-1/2">
|
||||
<h1 class="text-lg sm:text-xl md:text-2xl mb-1" style="line-height: 1.15; font-weight: 100">
|
||||
<h1 :data-type="ebookType" class="text-lg sm:text-xl md:text-2xl mb-1 data-[type=comic]:hidden" style="line-height: 1.15; font-weight: 100">
|
||||
<span style="font-weight: 600">{{ abTitle }}</span>
|
||||
<span v-if="abAuthor" style="display: inline"> – </span>
|
||||
<span v-if="abAuthor">{{ abAuthor }}</span>
|
||||
<span v-if="abAuthor" class="hidden md:inline"> – </span>
|
||||
<span v-if="abAuthor" class="hidden md:inline">{{ abAuthor }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-4 right-4 z-20">
|
||||
<span v-if="hasSettings" class="material-icons cursor-pointer text-2xl" @click="openSettings">settings</span>
|
||||
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
||||
<button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100">
|
||||
<span class="material-icons text-2xl">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" />
|
||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" />
|
||||
|
||||
<!-- TOC side nav -->
|
||||
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||
<div v-if="hasToC" class="w-96 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC">
|
||||
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC">
|
||||
<div class="p-4 h-full">
|
||||
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
||||
<div class="flex items-center mb-2">
|
||||
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
|
||||
<span class="material-icons text-2xl">arrow_back</span>
|
||||
</button>
|
||||
|
||||
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
|
||||
</div>
|
||||
<div class="tocContent">
|
||||
<ul>
|
||||
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||
<a :href="chapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||
<ul v-if="chapter.subitems.length">
|
||||
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
|
||||
<a :href="subchapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a>
|
||||
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -38,6 +50,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ereader settings modal -->
|
||||
<modals-modal v-model="showSettings" name="ereader-settings-modal" :width="500" :height="'unset'" :processing="false">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-3/4 overflow-hidden">
|
||||
<p class="text-xl md:text-3xl text-white truncate">{{ $strings.HeaderEreaderSettings }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-2 py-4 md:p-8 w-full text-base rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-40">
|
||||
<p class="text-lg">{{ $strings.LabelTheme }}:</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-40">
|
||||
<p class="text-lg">{{ $strings.LabelFontScale }}:</p>
|
||||
</div>
|
||||
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-40">
|
||||
<p class="text-lg">{{ $strings.LabelLineSpacing }}:</p>
|
||||
</div>
|
||||
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-40">
|
||||
<p class="text-lg">{{ $strings.LabelLayout }}:</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,8 +92,21 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
touchstartX: 0,
|
||||
touchstartY: 0,
|
||||
touchendX: 0,
|
||||
touchendY: 0,
|
||||
touchstartTime: 0,
|
||||
touchIdentifier: null,
|
||||
chapters: [],
|
||||
tocOpen: false
|
||||
tocOpen: false,
|
||||
showSettings: false,
|
||||
ereaderSettings: {
|
||||
theme: 'dark',
|
||||
fontScale: 100,
|
||||
lineSpacing: 115,
|
||||
spread: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -65,6 +125,34 @@ export default {
|
||||
this.$store.commit('setShowEReader', val)
|
||||
}
|
||||
},
|
||||
ereaderTheme() {
|
||||
if (this.isEpub) return this.ereaderSettings.theme
|
||||
return 'dark'
|
||||
},
|
||||
spreadItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelLayoutSinglePage,
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLayoutSplitPage,
|
||||
value: 'auto'
|
||||
}
|
||||
]
|
||||
},
|
||||
themeItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelThemeDark,
|
||||
value: 'dark'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelThemeLight,
|
||||
value: 'light'
|
||||
}
|
||||
]
|
||||
},
|
||||
componentName() {
|
||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||
@@ -75,11 +163,8 @@ export default {
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
hasToC() {
|
||||
return this.isEpub
|
||||
},
|
||||
hasSettings() {
|
||||
return false
|
||||
return this.isEpub
|
||||
},
|
||||
abTitle() {
|
||||
return this.mediaMetadata.title
|
||||
@@ -103,10 +188,18 @@ export default {
|
||||
return this.selectedLibraryItem.folderId
|
||||
},
|
||||
ebookFile() {
|
||||
// ebook file id is passed when reading a supplementary ebook
|
||||
if (this.ebookFileId) {
|
||||
return this.selectedLibraryItem.libraryFiles.find((lf) => lf.ino === this.ebookFileId)
|
||||
}
|
||||
return this.media.ebookFile
|
||||
},
|
||||
ebookFormat() {
|
||||
if (!this.ebookFile) return null
|
||||
// Use file extension for supplementary ebook
|
||||
if (!this.ebookFile.ebookFormat) {
|
||||
return this.ebookFile.metadata.ext.toLowerCase().slice(1)
|
||||
}
|
||||
return this.ebookFile.ebookFormat
|
||||
},
|
||||
ebookType() {
|
||||
@@ -130,14 +223,34 @@ export default {
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
keepProgress() {
|
||||
return this.$store.state.ereaderKeepProgress
|
||||
},
|
||||
ebookFileId() {
|
||||
return this.$store.state.ereaderFileId
|
||||
},
|
||||
isDarkTheme() {
|
||||
return this.ereaderSettings.theme === 'dark'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readerMounted() {
|
||||
if (this.isEpub) {
|
||||
this.loadEreaderSettings()
|
||||
}
|
||||
},
|
||||
settingsUpdated() {
|
||||
this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings)
|
||||
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
|
||||
},
|
||||
toggleToC() {
|
||||
this.tocOpen = !this.tocOpen
|
||||
this.chapters = this.$refs.readerComponent.chapters
|
||||
},
|
||||
openSettings() {},
|
||||
openSettings() {
|
||||
this.showSettings = true
|
||||
},
|
||||
hotkey(action) {
|
||||
if (!this.$refs.readerComponent) return
|
||||
|
||||
@@ -155,11 +268,72 @@ export default {
|
||||
prev() {
|
||||
if (this.$refs.readerComponent?.prev) this.$refs.readerComponent.prev()
|
||||
},
|
||||
handleGesture() {
|
||||
// Touch must be less than 1s. Must be > 60px drag and X distance > Y distance
|
||||
const touchTimeMs = Date.now() - this.touchstartTime
|
||||
if (touchTimeMs >= 1000) {
|
||||
console.log('Touch too long', touchTimeMs)
|
||||
return
|
||||
}
|
||||
|
||||
const touchDistanceX = Math.abs(this.touchendX - this.touchstartX)
|
||||
const touchDistanceY = Math.abs(this.touchendY - this.touchstartY)
|
||||
const touchDistance = Math.sqrt(Math.pow(this.touchstartX - this.touchendX, 2) + Math.pow(this.touchstartY - this.touchendY, 2))
|
||||
if (touchDistance < 60) {
|
||||
return
|
||||
}
|
||||
|
||||
if (touchDistanceX < 60 || touchDistanceY > touchDistanceX) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.touchendX < this.touchstartX) {
|
||||
this.next()
|
||||
}
|
||||
if (this.touchendX > this.touchstartX) {
|
||||
this.prev()
|
||||
}
|
||||
},
|
||||
touchstart(e) {
|
||||
// Ignore rapid touch
|
||||
if (this.touchstartTime && Date.now() - this.touchstartTime < 250) {
|
||||
return
|
||||
}
|
||||
|
||||
this.touchstartX = e.touches[0].screenX
|
||||
this.touchstartY = e.touches[0].screenY
|
||||
this.touchstartTime = Date.now()
|
||||
this.touchIdentifier = e.touches[0].identifier
|
||||
},
|
||||
touchend(e) {
|
||||
if (this.touchIdentifier !== e.changedTouches[0].identifier) {
|
||||
return
|
||||
}
|
||||
|
||||
this.touchendX = e.changedTouches[0].screenX
|
||||
this.touchendY = e.changedTouches[0].screenY
|
||||
this.handleGesture()
|
||||
},
|
||||
registerListeners() {
|
||||
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
||||
document.body.addEventListener('touchstart', this.touchstart)
|
||||
document.body.addEventListener('touchend', this.touchend)
|
||||
},
|
||||
unregisterListeners() {
|
||||
this.$eventBus.$off('reader-hotkey', this.hotkey)
|
||||
document.body.removeEventListener('touchstart', this.touchstart)
|
||||
document.body.removeEventListener('touchend', this.touchend)
|
||||
},
|
||||
loadEreaderSettings() {
|
||||
try {
|
||||
const settings = localStorage.getItem('ereaderSettings')
|
||||
if (settings) {
|
||||
this.ereaderSettings = JSON.parse(settings)
|
||||
this.settingsUpdated()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load ereader settings', error)
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.registerListeners()
|
||||
|
||||
@@ -235,7 +235,6 @@ export default {
|
||||
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
||||
})
|
||||
}
|
||||
console.log('Data', this.data)
|
||||
|
||||
this.monthLabels = []
|
||||
var lastMonth = null
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{{ $secondsToTimestamp(track.duration) }}
|
||||
</td>
|
||||
<td v-if="contextMenuItems.length" class="text-center">
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" menu-width="110px" @action="contextMenuAction" />
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" :menu-width="110" @action="contextMenuAction" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
},
|
||||
deleteLibraryFile() {
|
||||
const payload = {
|
||||
message: 'This will delete the file from your file system. Are you sure?',
|
||||
message: this.$strings.MessageConfirmDeleteFile,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
||||
<td>
|
||||
<div class="w-full flex flex-row items-center justify-center">
|
||||
<ui-btn v-if="backup.serverVersion" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
|
||||
|
||||
<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-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
|
||||
<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-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>
|
||||
<button aria-label="Download Backup" class="inline-flex material-icons text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
|
||||
|
||||
<button aria-label="Delete Backup" class="inline-flex material-icons text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -80,6 +80,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadBackup(backup) {
|
||||
this.$downloadFile(`${process.env.serverUrl}/api/backups/${backup.id}/download?token=${this.userToken}`)
|
||||
},
|
||||
confirm() {
|
||||
this.showConfirmApply = false
|
||||
|
||||
@@ -91,8 +94,9 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isBackingUp = false
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(this.$strings.ToastBackupRestoreFailed)
|
||||
console.error('Failed to apply backup', error)
|
||||
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
},
|
||||
deleteBackupClick(backup) {
|
||||
|
||||
87
client/components/tables/EbookFilesTable.vue
Normal file
87
client/components/tables/EbookFilesTable.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="w-full my-2">
|
||||
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
||||
<p class="pr-2 md:pr-4">{{ $strings.HeaderEbookFiles }}</p>
|
||||
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
||||
<span class="text-sm font-mono">{{ ebookFiles.length }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
|
||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
||||
<span class="material-icons text-4xl">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<div class="w-full" v-show="showFiles">
|
||||
<table class="text-sm tracksTable">
|
||||
<tr>
|
||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||
<th class="text-left px-4 w-24">
|
||||
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-icons-outlined text-sm align-middle">info</span></ui-tooltip>
|
||||
</th>
|
||||
<th v-if="showMoreColumn" class="text-center w-16"></th>
|
||||
</tr>
|
||||
<template v-for="file in ebookFiles">
|
||||
<tables-ebook-files-table-row :key="file.path" :libraryItemId="libraryItemId" :showFullPath="showFullPath" :file="file" @read="readEbook" />
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showFiles: false,
|
||||
showFullPath: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
libraryItemId() {
|
||||
return this.libraryItem.id
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
libraryIsAudiobooksOnly() {
|
||||
return this.$store.getters['libraries/getLibraryIsAudiobooksOnly']
|
||||
},
|
||||
showMoreColumn() {
|
||||
return this.userCanDelete || this.userCanDownload || (this.userCanUpdate && !this.libraryIsAudiobooksOnly)
|
||||
},
|
||||
ebookFiles() {
|
||||
return (this.libraryItem.libraryFiles || []).filter((lf) => lf.fileType === 'ebook')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readEbook(fileIno) {
|
||||
this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno })
|
||||
},
|
||||
clickBar() {
|
||||
this.showFiles = !this.showFiles
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
139
client/components/tables/EbookFilesTableRow.vue
Normal file
139
client/components/tables/EbookFilesTableRow.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-icons-outlined text-success align-text-bottom">check_circle</span></ui-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
{{ $bytesPretty(file.metadata.size) }}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<ui-icon-btn icon="auto_stories" outlined borderless icon-font-size="1.125rem" :size="8" @click="readEbook" />
|
||||
</td>
|
||||
<td v-if="contextMenuItems.length" class="text-center">
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" :menu-width="130" :processing="processing" @action="contextMenuAction" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItemId: String,
|
||||
showFullPath: Boolean,
|
||||
file: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
downloadUrl() {
|
||||
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.file.ino}/download?token=${this.userToken}`
|
||||
},
|
||||
isPrimary() {
|
||||
return !this.file.isSupplementary
|
||||
},
|
||||
libraryIsAudiobooksOnly() {
|
||||
return this.$store.getters['libraries/getLibraryIsAudiobooksOnly']
|
||||
},
|
||||
contextMenuItems() {
|
||||
const items = []
|
||||
if (this.userCanUpdate && !this.libraryIsAudiobooksOnly) {
|
||||
items.push({
|
||||
text: this.isPrimary ? this.$strings.LabelSetEbookAsSupplementary : this.$strings.LabelSetEbookAsPrimary,
|
||||
action: 'updateStatus'
|
||||
})
|
||||
}
|
||||
if (this.userCanDownload) {
|
||||
items.push({
|
||||
text: this.$strings.LabelDownload,
|
||||
action: 'download'
|
||||
})
|
||||
}
|
||||
if (this.userCanDelete) {
|
||||
items.push({
|
||||
text: this.$strings.ButtonDelete,
|
||||
action: 'delete'
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readEbook() {
|
||||
this.$emit('read', this.file.ino)
|
||||
},
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'delete') {
|
||||
this.deleteLibraryFile()
|
||||
} else if (action === 'download') {
|
||||
this.downloadLibraryFile()
|
||||
} else if (action === 'updateStatus') {
|
||||
this.updateEbookStatus()
|
||||
}
|
||||
},
|
||||
updateEbookStatus() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$patch(`/api/items/${this.libraryItemId}/ebook/${this.file.ino}/status`)
|
||||
.then(() => {
|
||||
this.$toast.success('Ebook updated')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update ebook', error)
|
||||
this.$toast.error('Failed to update ebook')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
deleteLibraryFile() {
|
||||
const payload = {
|
||||
message: this.$strings.MessageConfirmDeleteFile,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
|
||||
.then(() => {
|
||||
this.$toast.success('File deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete file', error)
|
||||
this.$toast.error('Failed to delete file')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
downloadLibraryFile() {
|
||||
this.$downloadFile(this.downloadUrl, this.file.metadata.filename)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :audio-file="selectedAudioFile" />
|
||||
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :library-item-id="libraryItemId" :audio-file="selectedAudioFile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,7 +38,6 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isMissing: Boolean,
|
||||
expanded: Boolean, // start expanded
|
||||
inModal: Boolean
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="contextMenuItems.length" class="text-center">
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" menu-width="110px" @action="contextMenuAction" />
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" :menu-width="110" @action="contextMenuAction" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
},
|
||||
deleteLibraryFile() {
|
||||
const payload = {
|
||||
message: 'This will delete the file from your file system. Are you sure?',
|
||||
message: this.$strings.MessageConfirmDeleteFile,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -70,7 +70,10 @@ export default {
|
||||
methods: {
|
||||
editItem(playlistItem) {
|
||||
if (playlistItem.episode) {
|
||||
this.$store.commit('globals/setSelectedEpisode', playlist.episode)
|
||||
const episodeIds = this.items.map((pi) => pi.episodeId)
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', episodeIds)
|
||||
this.$store.commit('setSelectedLibraryItem', playlistItem.libraryItem)
|
||||
this.$store.commit('globals/setSelectedEpisode', playlistItem.episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
} else {
|
||||
const itemIds = this.items.map((i) => i.libraryItemId)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :audio-file="selectedAudioFile" />
|
||||
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :library-item-id="libraryItemId" :audio-file="selectedAudioFile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
</td>
|
||||
<td class="text-sm">{{ user.type }}</td>
|
||||
<td class="hidden lg:table-cell">
|
||||
<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 v-if="usersOnline[user.id]?.session?.displayTitle">
|
||||
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.displayTitle || '' }}</p>
|
||||
<p class="truncate text-xs text-gray-300">{{ getDeviceInfoString(usersOnline[user.id].session.deviceInfo) }}</p>
|
||||
</div>
|
||||
<div v-else-if="user.latestSession?.displayTitle">
|
||||
<p class="truncate text-xs">Last: {{ user.latestSession.displayTitle || '' }}</p>
|
||||
<p class="truncate text-xs text-gray-300">{{ getDeviceInfoString(user.latestSession.deviceInfo) }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs font-mono hidden sm:table-cell">
|
||||
@@ -83,6 +87,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
if (deviceInfo.manufacturer && deviceInfo.model) return `${deviceInfo.manufacturer} ${deviceInfo.model}`
|
||||
|
||||
return `${deviceInfo.osName || 'Unknown'} ${deviceInfo.osVersion || ''} ${deviceInfo.browserName || ''}`
|
||||
},
|
||||
deleteUserClick(user) {
|
||||
if (this.isDeletingUser) return
|
||||
if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
|
||||
@@ -114,11 +124,12 @@ export default {
|
||||
},
|
||||
loadUsers() {
|
||||
this.$axios
|
||||
.$get('/api/users')
|
||||
.$get('/api/users?include=latestSession')
|
||||
.then((res) => {
|
||||
this.users = res.users.sort((a, b) => {
|
||||
return a.createdAt - b.createdAt
|
||||
})
|
||||
console.log('Loaded users', this.users)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
.$patch(`/api/me/progress/${this.book.id}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
|
||||
@@ -198,7 +198,6 @@ export default {
|
||||
.$patch(routepath, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
|
||||
@@ -183,7 +183,6 @@ export default {
|
||||
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
<template>
|
||||
<div class="w-full py-6">
|
||||
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
|
||||
<div class="flex items-center mb-4">
|
||||
<p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
|
||||
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
||||
<div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0">
|
||||
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
|
||||
<div class="inline-flex bg-white/5 px-1 mx-2 rounded-md text-sm text-gray-100">
|
||||
<p v-if="episodesList.length === episodes.length">{{ episodes.length }}</p>
|
||||
<p v-else>{{ episodesList.length }} / {{ episodes.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow hidden md:block" />
|
||||
<template v-if="isSelectionMode">
|
||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
|
||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 sm:ml-4" />
|
||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||
<div class="flex-grow md:hidden" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||
</template>
|
||||
<div class="flex items-center">
|
||||
<template v-if="isSelectionMode">
|
||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
|
||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" />
|
||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||
<div class="flex-grow md:hidden" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||
@@ -51,7 +58,6 @@ export default {
|
||||
selectedEpisodes: [],
|
||||
episodesToRemove: [],
|
||||
processing: false,
|
||||
quickMatchingEpisodes: false,
|
||||
search: null,
|
||||
searchTimeout: null,
|
||||
searchText: null
|
||||
@@ -71,6 +77,10 @@ export default {
|
||||
{
|
||||
text: 'Quick match all episodes',
|
||||
action: 'quick-match-episodes'
|
||||
},
|
||||
{
|
||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||
action: 'batch-mark-as-finished'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -157,14 +167,20 @@ export default {
|
||||
episodesList() {
|
||||
return this.episodesSorted.filter((episode) => {
|
||||
if (!this.searchText) return true
|
||||
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||
return episode.title?.toLowerCase().includes(this.searchText) || episode.subtitle?.toLowerCase().includes(this.searchText)
|
||||
})
|
||||
},
|
||||
selectedIsFinished() {
|
||||
// Find an item that is not finished, if none then all items finished
|
||||
return !this.selectedEpisodes.find((episode) => {
|
||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress || !itemProgress.isFinished
|
||||
return !this.selectedEpisodes.some((episode) => {
|
||||
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress?.isFinished
|
||||
})
|
||||
},
|
||||
allEpisodesFinished() {
|
||||
return !this.episodesSorted.some((episode) => {
|
||||
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress?.isFinished
|
||||
})
|
||||
},
|
||||
dateFormat() {
|
||||
@@ -187,17 +203,34 @@ export default {
|
||||
},
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'quick-match-episodes') {
|
||||
if (this.quickMatchingEpisodes) return
|
||||
if (this.processing) return
|
||||
|
||||
this.quickMatchAllEpisodes()
|
||||
} else if (action === 'batch-mark-as-finished') {
|
||||
if (this.processing) return
|
||||
|
||||
this.markAllEpisodesFinished()
|
||||
}
|
||||
},
|
||||
markAllEpisodesFinished() {
|
||||
const newIsFinished = !this.allEpisodesFinished
|
||||
const payload = {
|
||||
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
quickMatchAllEpisodes() {
|
||||
if (!this.mediaMetadata.feedUrl) {
|
||||
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||
return
|
||||
}
|
||||
this.quickMatchingEpisodes = true
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||
@@ -217,7 +250,7 @@ export default {
|
||||
this.$toast.error('Failed to match episodes')
|
||||
})
|
||||
}
|
||||
this.quickMatchingEpisodes = false
|
||||
this.processing = false
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
@@ -241,17 +274,19 @@ export default {
|
||||
this.$store.commit('addItemToQueue', queueItem)
|
||||
},
|
||||
toggleBatchFinished() {
|
||||
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
|
||||
},
|
||||
batchUpdateEpisodesFinished(episodes, newIsFinished) {
|
||||
this.processing = true
|
||||
var newIsFinished = !this.selectedIsFinished
|
||||
var updateProgressPayloads = this.selectedEpisodes.map((episode) => {
|
||||
|
||||
const updateProgressPayloads = episodes.map((episode) => {
|
||||
return {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
episodeId: episode.id,
|
||||
isFinished: newIsFinished
|
||||
}
|
||||
})
|
||||
|
||||
this.$axios
|
||||
return this.$axios
|
||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
<template>
|
||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu">
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons" :class="iconClass">more_vert</span>
|
||||
</button>
|
||||
<div v-else class="h-full w-full flex items-center justify-center">
|
||||
<widgets-loading-spinner />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<transition name="menu">
|
||||
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth }">
|
||||
<div v-show="showMenu" ref="menuWrapper" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<template v-if="item.subitems">
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-default" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50 -ml-px" :style="{ left: menuWidth, top: index * 29 + 'px' }">
|
||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||
<div
|
||||
v-if="mouseoverItemIndex === index"
|
||||
:key="`subitems-${index}`"
|
||||
@mouseover="mouseoverSubItemMenu(index)"
|
||||
@mouseleave="mouseleaveSubItemMenu(index)"
|
||||
class="absolute bg-bg border rounded-b-md border-black-200 shadow-lg z-50 -ml-px py-1"
|
||||
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
||||
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
||||
>
|
||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||
<p>{{ subitem.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||
<p>{{ item.text }}</p>
|
||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||
<p class="text-left">{{ item.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -41,9 +52,10 @@ export default {
|
||||
default: ''
|
||||
},
|
||||
menuWidth: {
|
||||
type: String,
|
||||
default: '192px'
|
||||
}
|
||||
type: Number,
|
||||
default: 192
|
||||
},
|
||||
processing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -52,12 +64,18 @@ export default {
|
||||
events: ['mousedown'],
|
||||
isActive: true
|
||||
},
|
||||
submenuWidth: 144,
|
||||
showMenu: false,
|
||||
mouseoverItemIndex: null,
|
||||
isOverSubItemMenu: false
|
||||
isOverSubItemMenu: false,
|
||||
openSubMenuLeft: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submenuLeftPos() {
|
||||
return this.openSubMenuLeft ? -(this.submenuWidth - 1) : this.menuWidth - 0.5
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
mouseoverSubItemMenu(index) {
|
||||
this.isOverSubItemMenu = true
|
||||
@@ -80,6 +98,12 @@ export default {
|
||||
clickShowMenu() {
|
||||
if (this.disabled) return
|
||||
this.showMenu = !this.showMenu
|
||||
this.$nextTick(() => {
|
||||
const boundingRect = this.$refs.menuWrapper?.getBoundingClientRect()
|
||||
if (boundingRect) {
|
||||
this.openSubMenuLeft = window.innerWidth - boundingRect.x < this.menuWidth + this.submenuWidth + 5
|
||||
}
|
||||
})
|
||||
},
|
||||
clickedOutside() {
|
||||
this.showMenu = false
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name" @click.stop.prevent="clickShowMenu">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
||||
aria-haspopup="listbox"
|
||||
:aria-expanded="showMenu"
|
||||
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
||||
@click.stop.prevent="clickShowMenu"
|
||||
>
|
||||
<div class="flex items-center justify-center sm:justify-start">
|
||||
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||
@@ -8,7 +16,7 @@
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="listbox">
|
||||
<template v-for="library in librariesFiltered">
|
||||
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||
<div class="flex items-center px-2">
|
||||
@@ -93,4 +101,10 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.librariesDropdownMenu {
|
||||
max-height: calc(100vh - 75px);
|
||||
}
|
||||
</style>
|
||||
86
client/components/ui/RangeInput.vue
Normal file
86
client/components/ui/RangeInput.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="inline-flex">
|
||||
<input v-model="input" type="range" :min="min" :max="max" :step="step" />
|
||||
|
||||
<p class="text-sm ml-2">{{ input }}%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: [String, Number],
|
||||
min: Number,
|
||||
max: Number,
|
||||
step: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
input: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input[type='range'] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type='range']:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* chromium */
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
background-color: rgb(0 0 0 / 0.25);
|
||||
border-radius: 9999px;
|
||||
height: 0.75rem;
|
||||
}
|
||||
input[type='range']::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-top: -0.25rem;
|
||||
border-radius: 9999px;
|
||||
background-color: rgb(255 255 255 / 0.7);
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
input[type='range']:focus::-webkit-slider-thumb {
|
||||
border: 1px solid #6b6b6b;
|
||||
outline: 3px solid #6b6b6b;
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
|
||||
/* firefox */
|
||||
input[type='range']::-moz-range-track {
|
||||
background-color: rgb(0 0 0 / 0.25);
|
||||
border-radius: 9999px;
|
||||
height: 0.75rem;
|
||||
}
|
||||
input[type='range']::-moz-range-thumb {
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
margin-top: -0.25rem;
|
||||
background-color: rgb(255 255 255 / 0.7);
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
input[type='range']:focus::-moz-range-thumb {
|
||||
border: 1px solid #6b6b6b;
|
||||
outline: 3px solid #6b6b6b;
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
|
||||
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :rows="rows" class="w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
value: [String, Number],
|
||||
label: String,
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 2
|
||||
|
||||
85
client/components/ui/ToggleBtns.vue
Normal file
85
client/components/ui/ToggleBtns.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="inline-flex toggle-btn-wrapper shadow-md">
|
||||
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-none relative border border-gray-600 px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
|
||||
{{ item.text }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
/**
|
||||
* [{ "text", "", "value": "" }]
|
||||
*/
|
||||
items: {
|
||||
type: Array,
|
||||
default: Object
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickBtn(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toggle-btn-wrapper .toggle-btn:first-child {
|
||||
border-top-left-radius: 0.375rem /* 6px */;
|
||||
border-bottom-left-radius: 0.375rem /* 6px */;
|
||||
}
|
||||
.toggle-btn-wrapper .toggle-btn:last-child {
|
||||
border-top-right-radius: 0.375rem /* 6px */;
|
||||
border-bottom-right-radius: 0.375rem /* 6px */;
|
||||
}
|
||||
.toggle-btn-wrapper .toggle-btn:first-child::before {
|
||||
border-top-left-radius: 0.375rem /* 6px */;
|
||||
border-bottom-left-radius: 0.375rem /* 6px */;
|
||||
}
|
||||
.toggle-btn-wrapper .toggle-btn:last-child::before {
|
||||
border-top-right-radius: 0.375rem /* 6px */;
|
||||
border-bottom-right-radius: 0.375rem /* 6px */;
|
||||
}
|
||||
|
||||
.toggle-btn-wrapper .toggle-btn:not(:first-child) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.toggle-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
.toggle-btn:hover:not(:disabled)::before {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.toggle-btn:hover:not(:disabled) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.toggle-btn.selected {
|
||||
color: white;
|
||||
}
|
||||
.toggle-btn.selected::before {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
button.toggle-btn:disabled::before {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -213,7 +213,9 @@ export default {
|
||||
// Reload HTML content
|
||||
this.$refs.trix.editor.loadHTML(newContent)
|
||||
// Move cursor to end of new content updated
|
||||
this.$refs.trix.editor.setSelectedRange(this.getContentEndPosition())
|
||||
if (this.autofocus) {
|
||||
this.$refs.trix.editor.setSelectedRange(this.getContentEndPosition())
|
||||
}
|
||||
},
|
||||
getContentEndPosition() {
|
||||
return this.$refs.trix.editor.getDocument().toString().length - 1
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
||||
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }" style="top: 0; left: 0">
|
||||
<template v-for="(item, index) in items">
|
||||
<template v-if="item.subitems">
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-default" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" :style="{ left: 143 + 'px', top: index * 28 + 'px' }">
|
||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.func, subitem.data)">
|
||||
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute bg-bg rounded-b-md border border-black-200 py-1 shadow-lg z-50" :class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'" :style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }">
|
||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.func, subitem.data)">
|
||||
<p>{{ subitem.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop="clickAction(item.func)">
|
||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop="clickAction(item.func)">
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,11 +33,18 @@ export default {
|
||||
events: ['mousedown'],
|
||||
isActive: true
|
||||
},
|
||||
submenuWidth: 144,
|
||||
menuWidth: 144,
|
||||
mouseoverItemIndex: null,
|
||||
isOverSubItemMenu: false
|
||||
isOverSubItemMenu: false,
|
||||
openSubMenuLeft: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submenuLeftPos() {
|
||||
return this.openSubMenuLeft ? -this.submenuWidth : this.menuWidth - 1.5
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
mouseoverSubItemMenu(index) {
|
||||
this.isOverSubItemMenu = true
|
||||
@@ -77,7 +84,14 @@ export default {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const boundingRect = this.$refs.wrapper?.getBoundingClientRect()
|
||||
if (boundingRect) {
|
||||
this.openSubMenuLeft = window.innerWidth - boundingRect.x < this.menuWidth + this.submenuWidth + 5
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
@@ -491,9 +491,9 @@ export default {
|
||||
}
|
||||
},
|
||||
checkActiveElementIsInput() {
|
||||
var activeElement = document.activeElement
|
||||
var inputs = ['input', 'select', 'button', 'textarea']
|
||||
return activeElement && inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1
|
||||
const activeElement = document.activeElement
|
||||
const inputs = ['input', 'select', 'button', 'textarea', 'trix-editor']
|
||||
return activeElement && inputs.some((i) => i === activeElement.tagName.toLowerCase())
|
||||
},
|
||||
getHotkeyName(e) {
|
||||
var keyCode = e.keyCode || e.which
|
||||
@@ -560,12 +560,6 @@ export default {
|
||||
.catch((err) => console.error(err))
|
||||
},
|
||||
initLocalStorage() {
|
||||
// If experimental features set in local storage
|
||||
var experimentalFeaturesSaved = localStorage.getItem('experimental')
|
||||
if (experimentalFeaturesSaved === '1') {
|
||||
this.$store.commit('setExperimentalFeatures', true)
|
||||
}
|
||||
|
||||
// Queue auto play
|
||||
var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
|
||||
this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
|
||||
|
||||
@@ -71,8 +71,8 @@ module.exports = {
|
||||
],
|
||||
|
||||
proxy: {
|
||||
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }
|
||||
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
|
||||
},
|
||||
|
||||
io: {
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.22",
|
||||
"version": "2.3.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.22",
|
||||
"version": "2.3.3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.22",
|
||||
"version": "2.3.3",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -112,17 +112,17 @@
|
||||
<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>
|
||||
<div class="w-20 text-center">{{ $strings.HeaderChapters }}</div>
|
||||
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
|
||||
</div>
|
||||
<template v-for="track in audioTracks">
|
||||
<div :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success bg-opacity-10' : ''">
|
||||
<div class="flex-grow">
|
||||
<div class="flex-grow max-w-[calc(100%-80px)] pr-2">
|
||||
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
|
||||
</div>
|
||||
<div class="w-20" style="min-width: 80px">
|
||||
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
|
||||
</div>
|
||||
<div class="w-20 flex justify-center" style="min-width: 80px">
|
||||
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px">
|
||||
<span v-if="(track.chapters || []).length" class="material-icons text-success text-sm">check</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<transition name="slide">
|
||||
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||
<div class="flex flex-wrap -mx-2">
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 64k)'" class="m-2 max-w-40" />
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 128k)'" class="m-2 max-w-40" />
|
||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="'Audio Channels (1 or 2)'" class="m-2 max-w-40" />
|
||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" />
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
showEncodeOptions: false,
|
||||
shouldBackupAudioFiles: true,
|
||||
encodingOptions: {
|
||||
bitrate: '64k',
|
||||
bitrate: '128k',
|
||||
channels: '2',
|
||||
codec: 'aac'
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@
|
||||
<div v-if="enableBackups" class="mb-6">
|
||||
<div class="flex items-center pl-6 mb-2">
|
||||
<span class="material-icons-outlined text-2xl text-black-50 mr-2">schedule</span>
|
||||
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span></div>
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span>
|
||||
</div>
|
||||
<div class="text-gray-100">{{ scheduleDescription }}</div>
|
||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer ml-2" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||
</div>
|
||||
|
||||
<div v-if="nextBackupDate" class="flex items-center pl-6 py-0.5 px-2">
|
||||
<span class="material-icons-outlined text-2xl text-black-50 mr-2">event</span>
|
||||
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span></div>
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span>
|
||||
</div>
|
||||
<div class="text-gray-100">{{ nextBackupDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,6 +52,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updatingServerSettings: false,
|
||||
@@ -98,7 +107,7 @@ export default {
|
||||
this.$toast.error('Invalid number of backups to keep')
|
||||
return
|
||||
}
|
||||
var updatePayload = {
|
||||
const updatePayload = {
|
||||
backupSchedule: this.enableBackups ? this.cronExpression : false,
|
||||
backupsToKeep: Number(this.backupsToKeep),
|
||||
maxBackupSize: Number(this.maxBackupSize)
|
||||
@@ -108,15 +117,15 @@ export default {
|
||||
updateServerSettings(payload) {
|
||||
this.updatingServerSettings = true
|
||||
this.$store
|
||||
.dispatch('updateServerSettings', payload)
|
||||
.then((success) => {
|
||||
console.log('Updated Server Settings', success)
|
||||
this.updatingServerSettings = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
})
|
||||
.dispatch('updateServerSettings', payload)
|
||||
.then((success) => {
|
||||
console.log('Updated Server Settings', success)
|
||||
this.updatingServerSettings = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
|
||||
@@ -34,10 +34,14 @@
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="fromInput" v-model="newSettings.fromAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsFromAddress" />
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="testInput" v-model="newSettings.testAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsTestAddress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<ui-btn :loading="sendingTest" :disabled="savingSettings || !newSettings.host" type="button" @click="sendTestClick">{{ $strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn v-if="hasUpdates" :disabled="savingSettings" type="button" @click="resetChanges">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-btn v-else :loading="sendingTest" :disabled="savingSettings || !newSettings.host" type="button" @click="sendTestClick">{{ $strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn :loading="savingSettings" :disabled="!hasUpdates" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
@@ -47,7 +51,7 @@
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderEReaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick">
|
||||
<app-settings-content :header-text="$strings.HeaderEreaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick">
|
||||
<table v-if="existingEReaderDevices.length" class="tracksTable my-4">
|
||||
<tr>
|
||||
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||
@@ -80,6 +84,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -93,6 +102,7 @@ export default {
|
||||
secure: true,
|
||||
user: null,
|
||||
pass: null,
|
||||
testAddress: null,
|
||||
fromAddress: null
|
||||
},
|
||||
newEReaderDevice: {
|
||||
@@ -117,6 +127,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetChanges() {
|
||||
this.newSettings = {
|
||||
...this.settings
|
||||
}
|
||||
},
|
||||
editDeviceClick(device) {
|
||||
this.selectedEReaderDevice = device
|
||||
this.showEReaderDeviceModal = true
|
||||
@@ -139,7 +154,7 @@ export default {
|
||||
}
|
||||
this.deletingDeviceName = device.name
|
||||
this.$axios
|
||||
.$patch(`/emails/ereader-devices`, payload)
|
||||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||
this.$toast.success('Device deleted')
|
||||
@@ -196,6 +211,7 @@ export default {
|
||||
secure: this.newSettings.secure,
|
||||
user: this.newSettings.user,
|
||||
pass: this.newSettings.pass,
|
||||
testAddress: this.newSettings.testAddress,
|
||||
fromAddress: this.newSettings.fromAddress
|
||||
}
|
||||
this.savingSettings = true
|
||||
|
||||
@@ -166,7 +166,8 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<!-- old experimental features -->
|
||||
<!-- <div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
|
||||
</div>
|
||||
|
||||
@@ -180,26 +181,6 @@
|
||||
</a>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-enable-e-reader" v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-enable-e-reader">{{ $strings.LabelSettingsEnableEReader }}</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.scannerUseTone" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseTone', val)" />
|
||||
<ui-tooltip text="Tone library for metadata">
|
||||
<p class="pl-4">
|
||||
Use Tone library for metadata
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,7 +192,6 @@
|
||||
<div class="flex-grow" />
|
||||
<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">
|
||||
@@ -268,6 +248,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isResettingLibraryItems: false,
|
||||
@@ -303,14 +288,6 @@ export default {
|
||||
providers() {
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
showExperimentalFeatures: {
|
||||
get() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('setExperimentalFeatures', val)
|
||||
}
|
||||
},
|
||||
dateFormats() {
|
||||
return this.$store.state.globals.dateFormats
|
||||
},
|
||||
@@ -390,23 +367,6 @@ export default {
|
||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
},
|
||||
resetLibraryItems() {
|
||||
if (confirm(this.$strings.MessageRemoveAllItemsWarning)) {
|
||||
this.isResettingLibraryItems = true
|
||||
this.$axios
|
||||
.$delete('/api/items/all')
|
||||
.then(() => {
|
||||
this.isResettingLibraryItems = false
|
||||
this.$toast.success('Successfully reset items')
|
||||
location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('failed to reset items', error)
|
||||
this.isResettingLibraryItems = false
|
||||
this.$toast.error('Failed to reset items - manually remove the /config/libraryItems folder')
|
||||
})
|
||||
}
|
||||
},
|
||||
purgeCache() {
|
||||
this.showConfirmPurgeCache = true
|
||||
},
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showLibraryModal: false,
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ redirect, store }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.state.libraries.currentLibraryId) {
|
||||
return redirect('/config')
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: null,
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
||||
@@ -104,7 +104,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
async asyncData({ store, redirect, app }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
return
|
||||
}
|
||||
|
||||
const users = await app.$axios
|
||||
.$get('/api/users')
|
||||
.then((res) => {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="flex mb-4 items-center">
|
||||
<h1 class="text-2xl">{{ $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>
|
||||
<ui-btn v-if="isAdminOrUp" :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">
|
||||
@@ -82,6 +82,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
@@ -116,7 +119,6 @@ export default {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
})
|
||||
console.log('Loaded user listening data', this.listeningStats)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -47,12 +47,6 @@
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
|
||||
|
||||
<div v-if="mediaProgressWithoutMedia.length" class="flex items-center py-2 mb-2">
|
||||
<p class="text-error">User has media progress for {{ mediaProgressWithoutMedia.length }} items that no longer exist.</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">{{ $strings.ButtonPurgeMediaProgress }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-16 text-left">{{ $strings.LabelItem }}</th>
|
||||
@@ -111,8 +105,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
listeningSessions: {},
|
||||
listeningStats: {},
|
||||
purgingMediaProgress: false
|
||||
listeningStats: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -134,9 +127,6 @@ export default {
|
||||
mediaProgressWithMedia() {
|
||||
return this.mediaProgress.filter((mp) => mp.media)
|
||||
},
|
||||
mediaProgressWithoutMedia() {
|
||||
return this.mediaProgress.filter((mp) => !mp.media)
|
||||
},
|
||||
totalListeningTime() {
|
||||
return this.listeningStats.totalTime || 0
|
||||
},
|
||||
@@ -176,24 +166,6 @@ export default {
|
||||
return []
|
||||
})
|
||||
console.log('Loaded user listening data', this.listeningSessions, this.listeningStats)
|
||||
},
|
||||
purgeMediaProgress() {
|
||||
this.purgingMediaProgress = true
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/users/${this.user.id}/purge-media-progress`)
|
||||
.then((updatedUser) => {
|
||||
console.log('Updated user', updatedUser)
|
||||
this.$toast.success('Media progress purged')
|
||||
this.user = updatedUser
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to purge media progress', error)
|
||||
this.$toast.error('Failed to purge media progress')
|
||||
})
|
||||
.finally(() => {
|
||||
this.purgingMediaProgress = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedAccount: null,
|
||||
|
||||
@@ -52,13 +52,6 @@
|
||||
<div class="hidden md:block flex-grow" />
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center">
|
||||
<span class="material-icons text-2xl">warning_amber</span>
|
||||
<p v-if="userIsAdminOrUp" class="ml-4">Book has no audio tracks but has an ebook. The experimental e-reader can be enabled in config.</p>
|
||||
<p v-else class="ml-4">Book has no audio tracks but has an ebook. The experimental e-reader must be enabled by a server admin.</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast episode downloads queue -->
|
||||
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||
<div class="flex items-center">
|
||||
@@ -122,7 +115,7 @@
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" menu-width="148px" @action="contextMenuAction">
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
|
||||
<template #default="{ showMenu, clickShowMenu, disabled }">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons">more_horiz</span>
|
||||
@@ -147,7 +140,9 @@
|
||||
|
||||
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
|
||||
|
||||
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item="libraryItem" class="mt-6" />
|
||||
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" class="mt-6" />
|
||||
|
||||
<tables-library-files-table v-if="libraryFiles.length" :library-item="libraryItem" class="mt-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,12 +195,6 @@ export default {
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
enableEReader() {
|
||||
return this.$store.getters['getServerSetting']('enableEReader')
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
@@ -257,7 +246,7 @@ export default {
|
||||
return this.tracks.length
|
||||
},
|
||||
showReadButton() {
|
||||
return this.ebookFile && (this.showExperimentalFeatures || this.enableEReader)
|
||||
return this.ebookFile
|
||||
},
|
||||
libraryId() {
|
||||
return this.libraryItem.libraryId
|
||||
@@ -320,6 +309,9 @@ export default {
|
||||
libraryFiles() {
|
||||
return this.libraryItem.libraryFiles || []
|
||||
},
|
||||
ebookFiles() {
|
||||
return this.libraryFiles.filter((lf) => lf.fileType === 'ebook')
|
||||
},
|
||||
ebookFile() {
|
||||
return this.media.ebookFile
|
||||
},
|
||||
@@ -330,9 +322,6 @@ export default {
|
||||
// Music track
|
||||
return this.media.audioFile
|
||||
},
|
||||
showExperimentalReadAlert() {
|
||||
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
||||
},
|
||||
description() {
|
||||
return this.mediaMetadata.description || ''
|
||||
},
|
||||
@@ -519,7 +508,7 @@ export default {
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' })
|
||||
},
|
||||
openEbook() {
|
||||
this.$store.commit('showEReader', this.libraryItem)
|
||||
this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: true })
|
||||
},
|
||||
toggleFinished(confirmed = false) {
|
||||
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
|
||||
@@ -544,7 +533,6 @@ export default {
|
||||
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="flex items-center">
|
||||
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
||||
<widgets-explicit-indicator :explicit="podcast.explicit" />
|
||||
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary"/>
|
||||
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary" />
|
||||
</div>
|
||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||
@@ -146,11 +146,15 @@ export default {
|
||||
async submitSearch(term) {
|
||||
this.processing = true
|
||||
this.termSearched = ''
|
||||
var results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => {
|
||||
let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => {
|
||||
console.error('Search request failed', error)
|
||||
return []
|
||||
})
|
||||
console.log('Got results', results)
|
||||
|
||||
// Filter out podcasts without an RSS feed
|
||||
results = results.filter((r) => r.feedUrl)
|
||||
|
||||
for (let result of results) {
|
||||
let podcast = this.existentPodcasts.find((p) => p.itunesId === result.id || p.title === result.title.toLowerCase())
|
||||
if (podcast) {
|
||||
@@ -164,7 +168,7 @@ export default {
|
||||
},
|
||||
async selectPodcast(podcast) {
|
||||
console.log('Selected podcast', podcast)
|
||||
if(podcast.existentId){
|
||||
if (podcast.existentId) {
|
||||
this.$router.push(`/item/${podcast.existentId}`)
|
||||
return
|
||||
}
|
||||
@@ -173,7 +177,7 @@ export default {
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
var payload = await this.$axios.$post(`/api/podcasts/feed`, {rssFeed: podcast.feedUrl}).catch((error) => {
|
||||
const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
|
||||
console.error('Failed to get feed', error)
|
||||
this.$toast.error('Failed to get podcast feed')
|
||||
return null
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
const series = await app.$axios.$get(`/api/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||
const series = await app.$axios.$get(`/api/libraries/${library.id}/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -74,9 +74,17 @@ export default {
|
||||
} else {
|
||||
this.$router.replace('/oops?message=No libraries available')
|
||||
}
|
||||
} else if (this.$route.query.redirect) {
|
||||
this.$router.replace(this.$route.query.redirect)
|
||||
} else {
|
||||
if (this.$route.query.redirect) {
|
||||
const isAdminUser = this.$store.getters['user/getIsAdminOrUp']
|
||||
const redirect = this.$route.query.redirect
|
||||
// If not admin user then do not redirect to config pages other than your stats
|
||||
if (isAdminUser || !redirect.startsWith('/config/') || redirect === '/config/stats') {
|
||||
this.$router.replace(redirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||
}
|
||||
}
|
||||
@@ -144,17 +152,17 @@ export default {
|
||||
this.error = null
|
||||
this.processing = true
|
||||
|
||||
var payload = {
|
||||
const payload = {
|
||||
username: this.username,
|
||||
password: this.password || ''
|
||||
}
|
||||
var authRes = await this.$axios.$post('/login', payload).catch((error) => {
|
||||
const authRes = await this.$axios.$post('/login', payload).catch((error) => {
|
||||
console.error('Failed', error.response)
|
||||
if (error.response) this.error = error.response.data
|
||||
else this.error = 'Unknown Error'
|
||||
return false
|
||||
})
|
||||
if (authRes && authRes.error) {
|
||||
if (authRes?.error) {
|
||||
this.error = authRes.error
|
||||
} else if (authRes) {
|
||||
this.setUser(authRes)
|
||||
@@ -162,7 +170,7 @@ export default {
|
||||
this.processing = false
|
||||
},
|
||||
checkAuth() {
|
||||
var token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) return false
|
||||
|
||||
this.processing = true
|
||||
|
||||
@@ -191,6 +191,7 @@ export default class PlayerHandler {
|
||||
|
||||
const payload = {
|
||||
deviceInfo: {
|
||||
clientName: 'Abs Web',
|
||||
deviceId: this.getDeviceId()
|
||||
},
|
||||
supportedMimeTypes: this.player.playableMimeTypes,
|
||||
@@ -281,6 +282,10 @@ export default class PlayerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First sync happens after 20 seconds
|
||||
* subsequent syncs happen every 10 seconds
|
||||
*/
|
||||
startPlayInterval() {
|
||||
clearInterval(this.playInterval)
|
||||
let lastTick = Date.now()
|
||||
@@ -293,7 +298,7 @@ export default class PlayerHandler {
|
||||
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
||||
lastTick = Date.now()
|
||||
this.listeningTimeSinceSync += exactTimeElapsed
|
||||
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 5 : 20
|
||||
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 10 : 20
|
||||
if (this.listeningTimeSinceSync >= TimeToWaitBeforeSync) {
|
||||
this.sendProgressSync(currentTime)
|
||||
}
|
||||
@@ -315,7 +320,7 @@ export default class PlayerHandler {
|
||||
}
|
||||
this.listeningTimeSinceSync = 0
|
||||
this.lastSyncTime = 0
|
||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 6000 }).catch((error) => {
|
||||
console.error('Failed to close session', error)
|
||||
})
|
||||
}
|
||||
@@ -335,12 +340,13 @@ export default class PlayerHandler {
|
||||
}
|
||||
|
||||
this.listeningTimeSinceSync = 0
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 3000 }).then(() => {
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 9000 }).then(() => {
|
||||
this.failedProgressSyncs = 0
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update session progress', error)
|
||||
// After 4 failed sync attempts show an alert toast
|
||||
this.failedProgressSyncs++
|
||||
if (this.failedProgressSyncs >= 2) {
|
||||
if (this.failedProgressSyncs >= 4) {
|
||||
this.ctx.showFailedProgressSyncs()
|
||||
this.failedProgressSyncs = 0
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ export default function ({ $axios, store, $config }) {
|
||||
if (bearerToken) {
|
||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
config.url = `/dev${config.url}`
|
||||
console.log('Making request to ' + config.url)
|
||||
}
|
||||
})
|
||||
|
||||
$axios.onError(error => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const SupportedFileTypes = {
|
||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
|
||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
info: ['nfo'],
|
||||
text: ['txt'],
|
||||
|
||||
@@ -34,7 +34,7 @@ Vue.prototype.$strings = { ...enUsStrings }
|
||||
|
||||
Vue.prototype.$getString = (key, subs) => {
|
||||
if (!Vue.prototype.$strings[key]) return ''
|
||||
if (subs && Array.isArray(subs) && subs.length) {
|
||||
if (subs?.length && Array.isArray(subs)) {
|
||||
return supplant(Vue.prototype.$strings[key], subs)
|
||||
}
|
||||
return Vue.prototype.$strings[key]
|
||||
|
||||
@@ -24,20 +24,20 @@ Vue.prototype.$formatJsDate = (jsdate, fnsFormat = 'MM/dd/yyyy HH:mm') => {
|
||||
return format(jsdate, fnsFormat)
|
||||
}
|
||||
Vue.prototype.$formatTime = (unixms, fnsFormat = 'HH:mm') => {
|
||||
if (!unixms) return ''
|
||||
return format(unixms, fnsFormat)
|
||||
if (!unixms) return ''
|
||||
return format(unixms, fnsFormat)
|
||||
}
|
||||
Vue.prototype.$formatJsTime = (jsdate, fnsFormat = 'HH:mm') => {
|
||||
if (!jsdate || !isDate(jsdate)) return ''
|
||||
return format(jsdate, fnsFormat)
|
||||
if (!jsdate || !isDate(jsdate)) return ''
|
||||
return format(jsdate, fnsFormat)
|
||||
}
|
||||
Vue.prototype.$formatDatetime = (unixms, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||
if (!unixms) return ''
|
||||
return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||
if (!unixms) return ''
|
||||
return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||
}
|
||||
Vue.prototype.$formatJsDatetime = (jsdate, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||
if (!jsdate || !isDate(jsdate)) return ''
|
||||
return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||
if (!jsdate || !isDate(jsdate)) return ''
|
||||
return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||
}
|
||||
Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
||||
var date = addDays(new Date(), daysToAdd)
|
||||
@@ -132,8 +132,10 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
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')
|
||||
@@ -147,6 +149,7 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
document.body.removeChild(el)
|
||||
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ export const getters = {
|
||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => {
|
||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
if (!libraryItem) return placeholder
|
||||
var media = libraryItem.media
|
||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
||||
const media = libraryItem.media
|
||||
if (!media?.coverPath || media.coverPath === placeholder) return placeholder
|
||||
|
||||
// Absolute URL covers (should no longer be used)
|
||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||
@@ -99,14 +99,14 @@ export const getters = {
|
||||
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||
},
|
||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null, raw = false) => {
|
||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
||||
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
if (!libraryItemId) return placeholder
|
||||
var userToken = rootGetters['user/getToken']
|
||||
const userToken = rootGetters['user/getToken']
|
||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
}
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
},
|
||||
getIsBatchSelectingMediaItems: (state) => {
|
||||
return state.selectedMediaItems.length
|
||||
|
||||
@@ -17,11 +17,12 @@ export const state = () => ({
|
||||
editPodcastModalTab: 'details',
|
||||
showEditModal: false,
|
||||
showEReader: false,
|
||||
ereaderKeepProgress: false,
|
||||
ereaderFileId: null,
|
||||
selectedLibraryItem: null,
|
||||
developerMode: false,
|
||||
processingBatch: false,
|
||||
previousPath: '/',
|
||||
showExperimentalFeatures: false,
|
||||
bookshelfBookIds: [],
|
||||
episodeTableEpisodeIds: [],
|
||||
openModal: null,
|
||||
@@ -210,8 +211,10 @@ export const mutations = {
|
||||
setEditPodcastModalTab(state, tab) {
|
||||
state.editPodcastModalTab = tab
|
||||
},
|
||||
showEReader(state, libraryItem) {
|
||||
showEReader(state, { libraryItem, keepProgress, fileId }) {
|
||||
state.selectedLibraryItem = libraryItem
|
||||
state.ereaderKeepProgress = keepProgress
|
||||
state.ereaderFileId = fileId
|
||||
|
||||
state.showEReader = true
|
||||
},
|
||||
@@ -227,10 +230,6 @@ export const mutations = {
|
||||
setProcessingBatch(state, val) {
|
||||
state.processingBatch = val
|
||||
},
|
||||
setExperimentalFeatures(state, val) {
|
||||
state.showExperimentalFeatures = val
|
||||
localStorage.setItem('experimental', val ? 1 : 0)
|
||||
},
|
||||
setOpenModal(state, val) {
|
||||
state.openModal = val
|
||||
},
|
||||
|
||||
@@ -57,6 +57,9 @@ export const getters = {
|
||||
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1
|
||||
return getters.getCurrentLibrarySettings.coverAspectRatio === Constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||
},
|
||||
getLibraryIsAudiobooksOnly: (state, getters) => {
|
||||
return !!getters.getCurrentLibrarySettings?.audiobooksOnly
|
||||
},
|
||||
getCollection: state => id => {
|
||||
return state.collections.find(c => c.id === id)
|
||||
},
|
||||
@@ -235,21 +238,23 @@ export const mutations = {
|
||||
if (!libraryItem || !state.filterData) return
|
||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||
/*
|
||||
var data = {
|
||||
structure of filterData:
|
||||
{
|
||||
authors: [],
|
||||
genres: [],
|
||||
tags: [],
|
||||
series: [],
|
||||
narrators: [],
|
||||
languages: []
|
||||
languages: [],
|
||||
publishers: []
|
||||
}
|
||||
*/
|
||||
var mediaMetadata = libraryItem.media.metadata
|
||||
const mediaMetadata = libraryItem.media.metadata
|
||||
|
||||
// Add/update book authors
|
||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
||||
if (mediaMetadata.authors?.length) {
|
||||
mediaMetadata.authors.forEach((author) => {
|
||||
var indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
||||
const indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
state.filterData.authors.splice(indexOf, 1, author)
|
||||
} else {
|
||||
@@ -260,9 +265,9 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add/update series
|
||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
||||
if (mediaMetadata.series?.length) {
|
||||
mediaMetadata.series.forEach((series) => {
|
||||
var indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
||||
const indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
||||
if (indexOf >= 0) {
|
||||
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
||||
} else {
|
||||
@@ -273,7 +278,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add genres
|
||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
||||
if (mediaMetadata.genres?.length) {
|
||||
mediaMetadata.genres.forEach((genre) => {
|
||||
if (!state.filterData.genres.includes(genre)) {
|
||||
state.filterData.genres.push(genre)
|
||||
@@ -283,7 +288,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if (libraryItem.media.tags && libraryItem.media.tags.length) {
|
||||
if (libraryItem.media.tags?.length) {
|
||||
libraryItem.media.tags.forEach((tag) => {
|
||||
if (!state.filterData.tags.includes(tag)) {
|
||||
state.filterData.tags.push(tag)
|
||||
@@ -293,7 +298,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add narrators
|
||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
||||
if (mediaMetadata.narrators?.length) {
|
||||
mediaMetadata.narrators.forEach((narrator) => {
|
||||
if (!state.filterData.narrators.includes(narrator)) {
|
||||
state.filterData.narrators.push(narrator)
|
||||
@@ -302,12 +307,16 @@ export const mutations = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add publishers
|
||||
if (mediaMetadata.publisher && !state.filterData.publishers.includes(mediaMetadata.publisher)) {
|
||||
state.filterData.publishers.push(mediaMetadata.publisher)
|
||||
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
// Add language
|
||||
if (mediaMetadata.language) {
|
||||
if (!state.filterData.languages.includes(mediaMetadata.language)) {
|
||||
state.filterData.languages.push(mediaMetadata.language)
|
||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
|
||||
state.filterData.languages.push(mediaMetadata.language)
|
||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
},
|
||||
setCollections(state, collections) {
|
||||
|
||||
@@ -33,22 +33,22 @@ export const getters = {
|
||||
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
||||
},
|
||||
getUserSetting: (state) => (key) => {
|
||||
return state.settings ? state.settings[key] : null
|
||||
return state.settings?.[key] || null
|
||||
},
|
||||
getUserCanUpdate: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.update : false
|
||||
return !!state.user?.permissions?.update
|
||||
},
|
||||
getUserCanDelete: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.delete : false
|
||||
return !!state.user?.permissions?.delete
|
||||
},
|
||||
getUserCanDownload: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
||||
return !!state.user?.permissions?.download
|
||||
},
|
||||
getUserCanUpload: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.upload : false
|
||||
return !!state.user?.permissions?.upload
|
||||
},
|
||||
getUserCanAccessAllLibraries: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.accessAllLibraries : false
|
||||
return !!state.user?.permissions?.accessAllLibraries
|
||||
},
|
||||
getLibrariesAccessible: (state, getters) => {
|
||||
if (!state.user) return []
|
||||
@@ -80,7 +80,7 @@ export const actions = {
|
||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||
settingsUpdate.orderBy = 'media.metadata.title'
|
||||
}
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||
if (invalidFilters.includes(filterByFirstPart)) {
|
||||
settingsUpdate.filterBy = 'all'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"ButtonAddChapters": "Kapitel hinzufügen",
|
||||
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||
"ButtonApply": "Anwenden",
|
||||
"ButtonApply": "Übernehmen",
|
||||
"ButtonApplyChapters": "Kapitel anwenden",
|
||||
"ButtonAuthors": "Autoren",
|
||||
"ButtonBrowseForFolder": "Ordnersuche",
|
||||
@@ -37,7 +37,7 @@
|
||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||
"ButtonNevermind": "Vergiss es",
|
||||
"ButtonNevermind": "Abbrechen",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Feed öffnen",
|
||||
"ButtonOpenManager": "Manager öffnen",
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Warteschlange",
|
||||
"HeaderEbookFiles": "E-Book Dateien",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEmailSettings": "Email Einstellungen",
|
||||
"HeaderEpisodes": "Episoden",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Geräte",
|
||||
"HeaderEreaderSettings": "Ereader Einstellungen",
|
||||
"HeaderFiles": "Dateien",
|
||||
"HeaderFindChapters": "Kapitel suchen",
|
||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
||||
"HeaderTableOfContents": "Inhaltsverzeichnis",
|
||||
"HeaderTools": "Werkzeuge",
|
||||
"HeaderUpdateAccount": "Konto aktualisieren",
|
||||
"HeaderUpdateAuthor": "Autor aktualisieren",
|
||||
@@ -192,7 +195,7 @@
|
||||
"LabelBooks": "Bücher",
|
||||
"LabelChangePassword": "Passwort ändern",
|
||||
"LabelChannels": "Kanäle",
|
||||
"LabelChapters": "Chapters",
|
||||
"LabelChapters": "Kapitel",
|
||||
"LabelChaptersFound": "gefundene Kapitel",
|
||||
"LabelChapterTitle": "Kapitelüberschrift",
|
||||
"LabelClosePlayer": "Player schließen",
|
||||
@@ -202,7 +205,7 @@
|
||||
"LabelComplete": "Vollständig",
|
||||
"LabelConfirmPassword": "Passwort bestätigen",
|
||||
"LabelContinueListening": "Weiterhören",
|
||||
"LabelContinueReading": "Continue Reading",
|
||||
"LabelContinueReading": "Lesen fortsetzen",
|
||||
"LabelContinueSeries": "Serien fortsetzen",
|
||||
"LabelCover": "Titelbild",
|
||||
"LabelCoverImageURL": "URL des Titelbildes",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
||||
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
||||
"LabelDownload": "Herunterladen",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Laufzeit",
|
||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbook": "E-Book",
|
||||
"LabelEbooks": "E-Books",
|
||||
"LabelEdit": "Bearbeiten",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsFromAddress": "Von Address",
|
||||
"LabelEmailSettingsSecure": "Sicherheit",
|
||||
"LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Addresse",
|
||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||
"LabelEnable": "Aktivieren",
|
||||
"LabelEnd": "Ende",
|
||||
@@ -238,7 +244,7 @@
|
||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "Datei",
|
||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
||||
"LabelFileBirthtime": "Datei erstellt",
|
||||
"LabelFileModified": "Datei geändert",
|
||||
"LabelFilename": "Dateiname",
|
||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "beendet",
|
||||
"LabelFolder": "Ordner",
|
||||
"LabelFolders": "Verzeichnisse",
|
||||
"LabelFontScale": "Schriftgröße",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Kategorie",
|
||||
"LabelGenres": "Kategorien",
|
||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||
"LabelHasEbook": "mit E-Book",
|
||||
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Stunde",
|
||||
"LabelIcon": "Symbol",
|
||||
@@ -266,7 +275,7 @@
|
||||
"LabelIntervalEveryDay": "Jeden Tag",
|
||||
"LabelIntervalEveryHour": "Jede Stunde",
|
||||
"LabelInvalidParts": "Ungültige Teile",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelInvert": "Umkehren",
|
||||
"LabelItem": "Medium",
|
||||
"LabelLanguage": "Sprache",
|
||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Zuletzt angesehen",
|
||||
"LabelLastTime": "Letztes Mal",
|
||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Eine Seite",
|
||||
"LabelLayoutSplitPage": "Geteilte Seite",
|
||||
"LabelLess": "Weniger",
|
||||
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
||||
"LabelLibrary": "Bibliothek",
|
||||
"LabelLibraryItem": "Bibliothekseintrag",
|
||||
"LabelLibraryName": "Bibliotheksname",
|
||||
"LabelLimit": "Begrenzung",
|
||||
"LabelLineSpacing": "Zeilenabstand",
|
||||
"LabelListenAgain": "Erneut anhören",
|
||||
"LabelLogLevelDebug": "Fehlersuche",
|
||||
"LabelLogLevelInfo": "Informationen",
|
||||
@@ -295,7 +308,7 @@
|
||||
"LabelMissing": "Fehlend",
|
||||
"LabelMissingParts": "Fehlende Teile",
|
||||
"LabelMore": "Mehr",
|
||||
"LabelMoreInfo": "More Info",
|
||||
"LabelMoreInfo": "Mehr Info",
|
||||
"LabelName": "Name",
|
||||
"LabelNarrator": "Erzähler",
|
||||
"LabelNarrators": "Erzähler",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Neues Passwort",
|
||||
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
||||
"LabelNotes": "Hinweise",
|
||||
"LabelNotFinished": "nicht beendet",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||
"LabelPrimaryEbook": "Haupt-E-Book",
|
||||
"LabelProgress": "Fortschritt",
|
||||
"LabelProvider": "Anbieter",
|
||||
"LabelPubDate": "Veröffentlichungsdatum",
|
||||
"LabelPublisher": "Herausgeber",
|
||||
"LabelPublishYear": "Jahr",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelRead": "Lesen",
|
||||
"LabelReadAgain": "Nocheinmal Lesen",
|
||||
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||
"LabelRecentSeries": "Aktuelle Serien",
|
||||
"LabelRecommended": "Empfohlen",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Titel",
|
||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||
"LabelSeason": "Staffel",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
|
||||
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
|
||||
"LabelSendEbookToDevice": "E-Book senden an...",
|
||||
"LabelSequence": "Reihenfolge",
|
||||
"LabelSeries": "Serien",
|
||||
"LabelSeriesName": "Serienname",
|
||||
"LabelSeriesProgress": "Serienfortschritt",
|
||||
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch",
|
||||
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung",
|
||||
"LabelSettingsAudiobooksOnly": "nur Hörbücher",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Wenn Sie diese Einstellung aktivieren, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||
"LabelSettingsDateFormat": "Datumsformat",
|
||||
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
||||
"LabelSettingsDisableWatcherHelp": "Deaktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
||||
"LabelSettingsEnableEReader": "E-Reader für alle Benutzer aktivieren",
|
||||
"LabelSettingsEnableEReaderHelp": "Der E-Reader befindet sich noch in der Entwicklung, aber mit dieser Einstellung können Sie ihn für alle Benutzer aktivieren (oder aktivieren Sie die Option \"Experimentelle Funktionen\", dann Sie ihn nur selbst verwenden)",
|
||||
"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 Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
|
||||
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
||||
"LabelTasks": "Laufende Aufgaben",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Basiszeit",
|
||||
"LabelTimeListened": "Gehörte Zeit",
|
||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||
"MessageCheckingCron": "Überprüfe Cron...",
|
||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sind Sie sicher, dass Sie alle Episoden als abgeschlossen markieren möchten?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Sind Sie sicher, dass Sie alle Episoden als nicht abgeschlossen markieren möchten?",
|
||||
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
||||
@@ -506,7 +535,7 @@
|
||||
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "Sind Sie sicher, dass sie {0} ebook \"{1}\" auf das Gerät \"{2}\" senden wollen?",
|
||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||
"MessageM4BFinished": "M4B beendet!",
|
||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||
"MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren",
|
||||
"MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren",
|
||||
"MessageMarkAsFinished": "Als beendet markieren",
|
||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
||||
"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 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": "Aus der Abspielwarteliste löschen",
|
||||
@@ -661,8 +691,8 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
|
||||
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||
@@ -672,4 +702,4 @@
|
||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Episodes",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Files",
|
||||
"HeaderFindChapters": "Find Chapters",
|
||||
"HeaderIgnoredFiles": "Ignored Files",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||
"HeaderStatsTop10Authors": "Top 10 Authors",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderUpdateAccount": "Update Account",
|
||||
"HeaderUpdateAuthor": "Update Author",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Disc from Filename",
|
||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Duration",
|
||||
"LabelDurationFound": "Duration found:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Enable",
|
||||
"LabelEnd": "End",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folders",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Hour",
|
||||
"LabelIcon": "Icon",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Last Seen",
|
||||
"LabelLastTime": "Last Time",
|
||||
"LabelLastUpdate": "Last Update",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Listen Again",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Progress",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRecommended": "Recommended",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Search Title",
|
||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"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",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tasks Running",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
"LabelTimeListenedToday": "Time Listened Today",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"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.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"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",
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||
"HeaderDetails": "Detalles",
|
||||
"HeaderDownloadQueue": "Lista de Descarga",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Episodios",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Elemento",
|
||||
"HeaderFindChapters": "Buscar Capitulo",
|
||||
"HeaderIgnoredFiles": "Ignorar Elemento",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Sesiones Recientes",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autores",
|
||||
"HeaderStatsTop5Genres": "Top 5 Géneros",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Herramientas",
|
||||
"HeaderUpdateAccount": "Actualizar Cuenta",
|
||||
"HeaderUpdateAuthor": "Actualizar Autor",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
|
||||
"LabelDiscFromMetadata": "Disco a partir de Metadata",
|
||||
"LabelDownload": "Descargar",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Duración",
|
||||
"LabelDurationFound": "Duración Comprobada:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Editar",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Portada Integrada",
|
||||
"LabelEnable": "Habilitar",
|
||||
"LabelEnd": "Fin",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Terminado",
|
||||
"LabelFolder": "Carpeta",
|
||||
"LabelFolders": "Carpetas",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Formato",
|
||||
"LabelGenre": "Genero",
|
||||
"LabelGenres": "Géneros",
|
||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Hora",
|
||||
"LabelIcon": "Icono",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Ultima Vez Visto",
|
||||
"LabelLastTime": "Ultima Vez",
|
||||
"LabelLastUpdate": "Ultima Actualización",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Menos",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
||||
"LabelLibrary": "Biblioteca",
|
||||
"LabelLibraryItem": "Elemento de Biblioteca",
|
||||
"LabelLibraryName": "Nombre de Biblioteca",
|
||||
"LabelLimit": "Limites",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Escuchar Otra Vez",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Nueva Contraseña",
|
||||
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
||||
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notas",
|
||||
"LabelNotFinished": "No Terminado",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
||||
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Progreso",
|
||||
"LabelProvider": "Proveedor",
|
||||
"LabelPubDate": "Fecha de Publicación",
|
||||
"LabelPublisher": "Editor",
|
||||
"LabelPublishYear": "Año de Publicación",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Agregado Reciente",
|
||||
"LabelRecentSeries": "Series Recientes",
|
||||
"LabelRecommended": "Recomendados",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Buscar Titulo",
|
||||
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
||||
"LabelSeason": "Temporada",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Secuencia",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Nombre de la Serie",
|
||||
"LabelSeriesProgress": "Progreso de la Serie",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"LabelSettingsBookshelfViewHelp": "Diseño Skeumorphic con Estantes de Madera",
|
||||
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
|
||||
"LabelSettingsDateFormat": "Formato de Fecha",
|
||||
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
|
||||
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función automática de agregar/actualizar los elementos, cuando se detecta cambio en los archivos. *Require Reiniciar el Servidor",
|
||||
"LabelSettingsEnableEReader": "Habilitar e-reader para todos los usuarios",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader sigue en proceso, pero use esta configuración para hacerlo disponible a todos los usuarios. (o use las \"Funciones Experimentales\" para habilitarla para solo este usuario)",
|
||||
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo sobre las que esperamos sus comentarios y experiencia. Haga click aquí para abrir una conversación en Github.",
|
||||
"LabelSettingsFindCovers": "Buscar Portadas",
|
||||
"LabelSettingsFindCoversHelp": "Si tu audiolibro no tiene una portada incluida o la portada no esta dentro de la carpeta, el escaneador tratara de encontrar una portada.<br>Nota: Esto extenderá el tiempo de escaneo",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "La pagina de inicio usa la vista de librero",
|
||||
"LabelSettingsLibraryBookshelfView": "La biblioteca usa la vista de librero",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Usar Markers de multimedia en Overdrive para estos capítulos",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tareas Corriendo",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Tiempo Escuchando",
|
||||
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Esta seguro que desea eliminar el respaldo {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Fallo!",
|
||||
"MessageM4BFinished": "M4B Terminado!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Marcar como Terminado",
|
||||
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
||||
"MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Crear lista de reproducción a partir de colección",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar que coincida",
|
||||
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la configuración 'Prefer matched metadata' del servidor este habilita.",
|
||||
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
|
||||
"MessageRemoveChapter": "Remover capítulos",
|
||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"ButtonRemoveAll": "Supprimer tout",
|
||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||
"ButtonRemoveFromContinueReading": "Ne plus continuer à lire",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
||||
"ButtonReScan": "Nouvelle analyse",
|
||||
"ButtonReset": "Réinitialiser",
|
||||
@@ -87,7 +87,7 @@
|
||||
"HeaderAdvanced": "Avancé",
|
||||
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
|
||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||
"HeaderAudioTracks": "Pistes zudio",
|
||||
"HeaderAudioTracks": "Pistes audio",
|
||||
"HeaderBackups": "Sauvegardes",
|
||||
"HeaderChangePassword": "Modifier le mot de passe",
|
||||
"HeaderChapters": "Chapitres",
|
||||
@@ -95,13 +95,15 @@
|
||||
"HeaderCollection": "Collection",
|
||||
"HeaderCollectionItems": "Entrées de la Collection",
|
||||
"HeaderCover": "Couverture",
|
||||
"HeaderCurrentDownloads": "File d’attente de téléchargement",
|
||||
"HeaderCurrentDownloads": "Téléchargements en cours",
|
||||
"HeaderDetails": "Détails",
|
||||
"HeaderDownloadQueue": "Queue de téléchargement",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||
"HeaderEbookFiles": "Fichier des livres numériques",
|
||||
"HeaderEmail": "Courriels",
|
||||
"HeaderEmailSettings": "Configuration des courriels",
|
||||
"HeaderEpisodes": "Épisodes",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Lecteur de livres numériques",
|
||||
"HeaderEreaderSettings": "Options Ereader",
|
||||
"HeaderFiles": "Fichiers",
|
||||
"HeaderFindChapters": "Trouver les chapitres",
|
||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Sessions récentes",
|
||||
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Outils",
|
||||
"HeaderUpdateAccount": "Mettre à jour le compte",
|
||||
"HeaderUpdateAuthor": "Mettre à jour l’auteur",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||
"LabelDownload": "Téléchargement",
|
||||
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
||||
"LabelDuration": "Durée",
|
||||
"LabelDurationFound": "Durée trouvée :",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbook": "Livre numérique",
|
||||
"LabelEbooks": "Livres numériques",
|
||||
"LabelEdit": "Modifier",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmail": "Courriel",
|
||||
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||
"LabelEmailSettingsSecure": "Sécurisé",
|
||||
"LabelEmailSettingsSecureHelp": "Si coché, la connexion utilisera TLS lors de la connexion au serveur. Sinon TLS est utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, cochez si vous vous connectez au port 465. Décochez pour le port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||
"LabelEnable": "Activer",
|
||||
"LabelEnd": "Fin",
|
||||
@@ -236,9 +242,9 @@
|
||||
"LabelEpisodeType": "Type de l’épisode",
|
||||
"LabelExample": "Exemple",
|
||||
"LabelExplicit": "Restriction",
|
||||
"LabelFeedURL": "URL deu flux",
|
||||
"LabelFeedURL": "URL du flux",
|
||||
"LabelFile": "Fichier",
|
||||
"LabelFileBirthtime": "Creation du fichier",
|
||||
"LabelFileBirthtime": "Création du fichier",
|
||||
"LabelFileModified": "Modification du fichier",
|
||||
"LabelFilename": "Nom de fichier",
|
||||
"LabelFilterByUser": "Filtrer par l’utilisateur",
|
||||
@@ -246,17 +252,20 @@
|
||||
"LabelFinished": "Fini(e)",
|
||||
"LabelFolder": "Dossier",
|
||||
"LabelFolders": "Dossiers",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Suppression du fichier",
|
||||
"LabelHost": "Host",
|
||||
"LabelHasEbook": "Dispose d’un livre numérique",
|
||||
"LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire",
|
||||
"LabelHost": "Hôte",
|
||||
"LabelHour": "Heure",
|
||||
"LabelIcon": "Icone",
|
||||
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
||||
"LabelIncomplete": "Incomplet",
|
||||
"LabelInProgress": "En cours",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelInterval": "Intervalle",
|
||||
"LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé",
|
||||
"LabelIntervalEvery12Hours": "Toutes les 12 heures",
|
||||
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
|
||||
@@ -266,7 +275,7 @@
|
||||
"LabelIntervalEveryDay": "Tous les jours",
|
||||
"LabelIntervalEveryHour": "Toutes les heures",
|
||||
"LabelInvalidParts": "Parties invalides",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelInvert": "Inverser",
|
||||
"LabelItem": "Article",
|
||||
"LabelLanguage": "Langue",
|
||||
"LabelLanguageDefaultServer": "Langue par défaut",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Vu dernièrement",
|
||||
"LabelLastTime": "Progression",
|
||||
"LabelLastUpdate": "Dernière mise à jour",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Moins",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur",
|
||||
"LabelLibrary": "Bibliothèque",
|
||||
"LabelLibraryItem": "Article de bibliothèque",
|
||||
"LabelLibraryName": "Nom de la bibliothèque",
|
||||
"LabelLimit": "Limite",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Écouter à nouveau",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,16 +318,17 @@
|
||||
"LabelNewPassword": "Nouveau mot de passe",
|
||||
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
|
||||
"LabelNextScheduledRun": "Prochain lancement prévu",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Non terminé(e)",
|
||||
"LabelNotificationAppriseURL": "URL(s) d’apprise",
|
||||
"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.",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications 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",
|
||||
@@ -338,20 +352,23 @@
|
||||
"LabelPodcastType": "Type de Podcast",
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
|
||||
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de données iTunes et Google podcast",
|
||||
"LabelPrimaryEbook": "Premier livre numérique",
|
||||
"LabelProgress": "Progression",
|
||||
"LabelProvider": "Fournisseur",
|
||||
"LabelPubDate": "Date de publication",
|
||||
"LabelPublisher": "Éditeur",
|
||||
"LabelPublishYear": "Année d’édition",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelRead": "Lire",
|
||||
"LabelReadAgain": "Lire à nouveau",
|
||||
"LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression",
|
||||
"LabelRecentlyAdded": "Derniers ajouts",
|
||||
"LabelRecentSeries": "Séries récentes",
|
||||
"LabelRecommended": "Recommandé",
|
||||
"LabelRegion": "Région",
|
||||
"LabelReleaseDate": "Date de parution",
|
||||
"LabelRemoveCover": "Supprimer la couverture",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Titre de recherche",
|
||||
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
||||
"LabelSeason": "Saison",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
|
||||
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
|
||||
"LabelSequence": "Séquence",
|
||||
"LabelSeries": "Séries",
|
||||
"LabelSeriesName": "Nom de la série",
|
||||
"LabelSeriesProgress": "Progression de séries",
|
||||
"LabelSettingsBookshelfViewHelp": "Interface Skeuomorphic avec une étagère en bois",
|
||||
"LabelSetEbookAsPrimary": "Définir comme principale",
|
||||
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers “ ebook ”, à moins qu’ils ne se trouvent dans un dossier de livres audio, auquel cas ils seront définis comme des livres numériques supplémentaires.",
|
||||
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
||||
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||
"LabelSettingsDateFormat": "Format de date",
|
||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers 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.",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.",
|
||||
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
||||
"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",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tâches en cours",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Base de temps",
|
||||
"LabelTimeListened": "Temps d’écoute",
|
||||
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
||||
@@ -481,32 +507,35 @@
|
||||
"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",
|
||||
"MessageBookshelfNoSeries": "Vous n’avez aucune série",
|
||||
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
||||
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
||||
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
||||
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
||||
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
||||
"MessageChapterStartIsAfter": "Le premier chapitre est situé au début de votre livre audio",
|
||||
"MessageCheckingCron": "Vérification du cron…",
|
||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
||||
"MessageConfirmDeleteFile": "Cela Le fichier sera supprimer de votre système. Êtes-vous sûr ?",
|
||||
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
||||
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » vers « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||
"MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà « {0} ».",
|
||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » vers « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} \"{1}\" à l’appareil \"{2}\"?",
|
||||
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||
"MessageEmbedFinished": "Intégration Terminée !",
|
||||
@@ -525,8 +554,10 @@
|
||||
"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.",
|
||||
"MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés",
|
||||
"MessageMarkAllEpisodesNotFinished": "Marquer tous les épisodes non terminés",
|
||||
"MessageMarkAsFinished": "Marquer comme terminé",
|
||||
"MessageMarkAsNotFinished": "Marquer comme non 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": "Aucune piste audio",
|
||||
"MessageNoAuthors": "Aucun auteur",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n’a pas d’URL de flux RSS à utiliser pour la correspondance",
|
||||
"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. Souhaitez-vous continuer ?",
|
||||
"MessageRemoveChapter": "Supprimer le chapitre",
|
||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d’écoute",
|
||||
@@ -594,7 +624,7 @@
|
||||
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
||||
"NoteUploaderOnlyAudioFiles": "Si vous téléverser uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
||||
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
||||
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
|
||||
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||
@@ -661,8 +691,8 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||
"ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil",
|
||||
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
|
||||
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||
@@ -672,4 +702,4 @@
|
||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Episodes",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Files",
|
||||
"HeaderFindChapters": "Find Chapters",
|
||||
"HeaderIgnoredFiles": "Ignored Files",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||
"HeaderStatsTop10Authors": "Top 10 Authors",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderUpdateAccount": "Update Account",
|
||||
"HeaderUpdateAuthor": "Update Author",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Disc from Filename",
|
||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Duration",
|
||||
"LabelDurationFound": "Duration found:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Enable",
|
||||
"LabelEnd": "End",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folders",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Hour",
|
||||
"LabelIcon": "Icon",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Last Seen",
|
||||
"LabelLastTime": "Last Time",
|
||||
"LabelLastUpdate": "Last Update",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Listen Again",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Progress",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRecommended": "Recommended",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Search Title",
|
||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"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",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tasks Running",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
"LabelTimeListenedToday": "Time Listened Today",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"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.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"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",
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Episodes",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Files",
|
||||
"HeaderFindChapters": "Find Chapters",
|
||||
"HeaderIgnoredFiles": "Ignored Files",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||
"HeaderStatsTop10Authors": "Top 10 Authors",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderUpdateAccount": "Update Account",
|
||||
"HeaderUpdateAuthor": "Update Author",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Disc from Filename",
|
||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Duration",
|
||||
"LabelDurationFound": "Duration found:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Enable",
|
||||
"LabelEnd": "End",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folders",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Hour",
|
||||
"LabelIcon": "Icon",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Last Seen",
|
||||
"LabelLastTime": "Last Time",
|
||||
"LabelLastUpdate": "Last Update",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Listen Again",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Progress",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRecommended": "Recommended",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Search Title",
|
||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"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",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tasks Running",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
"LabelTimeListenedToday": "Time Listened Today",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"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.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"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",
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderDetails": "Detalji",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Epizode",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Datoteke",
|
||||
"HeaderFindChapters": "Pronađi poglavlja",
|
||||
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Nedavne sesije",
|
||||
"HeaderStatsTop10Authors": "Top 10 autora",
|
||||
"HeaderStatsTop5Genres": "Top 5 žanrova",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Alati",
|
||||
"HeaderUpdateAccount": "Aktualiziraj Korisnički račun",
|
||||
"HeaderUpdateAuthor": "Aktualiziraj autora",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "CD iz imena datoteke",
|
||||
"LabelDiscFromMetadata": "CD iz metapodataka",
|
||||
"LabelDownload": "Preuzmi",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Trajanje",
|
||||
"LabelDurationFound": "Pronađeno trajanje:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Uredi",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Uključi",
|
||||
"LabelEnd": "Kraj",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Finished",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Folderi",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Žanrovi",
|
||||
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Sat",
|
||||
"LabelIcon": "Ikona",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Zadnje pogledano",
|
||||
"LabelLastTime": "Prošli put",
|
||||
"LabelLastUpdate": "Zadnja aktualizacija",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Manje",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryItem": "Stavka biblioteke",
|
||||
"LabelLibraryName": "Ime biblioteke",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Slušaj ponovno",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Nova lozinka",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Bilješke",
|
||||
"LabelNotFinished": "Nedovršeno",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Napredak",
|
||||
"LabelProvider": "Dobavljač",
|
||||
"LabelPubDate": "Datam izdavanja",
|
||||
"LabelPublisher": "Izdavač",
|
||||
"LabelPublishYear": "Godina izdavanja",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Nedavno dodano",
|
||||
"LabelRecentSeries": "Nedavne serije",
|
||||
"LabelRecommended": "Recommended",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Traži naslov",
|
||||
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
|
||||
"LabelSeason": "Sezona",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sekvenca",
|
||||
"LabelSeries": "Serije",
|
||||
"LabelSeriesName": "Ime serije",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorfski (što god to bilo) dizajn sa drvenim policama",
|
||||
"LabelSettingsChromecastSupport": "Chromecast podrška",
|
||||
"LabelSettingsDateFormat": "Format datuma",
|
||||
"LabelSettingsDisableWatcher": "Isključi Watchera",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Isključi folder watchera za biblioteku",
|
||||
"LabelSettingsDisableWatcherHelp": "Isključi automatsko dodavanje/aktualiziranje stavci ako su promjene prepoznate. *Potreban restart servera",
|
||||
"LabelSettingsEnableEReader": "Uključi e-readere za sve korisnike",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader je i dalje rad u tijeku, ali s ovom postavkom ga možete uključiti za sve korisnike (ili koristi \"Eksperimentalni features\" toggle da bi uključio postavku samo za sebe)",
|
||||
"LabelSettingsExperimentalFeatures": "Eksperimentalni features",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.",
|
||||
"LabelSettingsFindCovers": "Pronađi covers",
|
||||
"LabelSettingsFindCoversHelp": "Ako audiobook nema embedani cover or a cover sliku unutar foldera, skener će probati pronaći cover.<br>Bilješka: Ovo će produžiti trjanje skeniranja",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
|
||||
"LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Koristi Overdrive Media Markers za poglavlja",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tasks Running",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Vremena odslušano",
|
||||
"LabelTimeListenedToday": "Vremena odslušano danas",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.",
|
||||
"MessageCheckingCron": "Provjeravam cron...",
|
||||
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B neuspješan!",
|
||||
"MessageM4BFinished": "M4B završio!",
|
||||
"MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Označi kao završeno",
|
||||
"MessageMarkAsNotFinished": "Označi kao nezavršeno",
|
||||
"MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"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",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||
"ButtonRemoveFromContinueReading": "Rimuovi per proseguire la lettura",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||
"ButtonReScan": "Ri-scansiona",
|
||||
"ButtonReset": "Reset",
|
||||
@@ -95,13 +95,15 @@
|
||||
"HeaderCollection": "Raccolta",
|
||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderCurrentDownloads": "Download Correnti",
|
||||
"HeaderDetails": "Dettagli",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Episodi",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Dispositivo Ereader",
|
||||
"HeaderEreaderSettings": "Impostazioni Ereader",
|
||||
"HeaderFiles": "File",
|
||||
"HeaderFindChapters": "Trova Capitoli",
|
||||
"HeaderIgnoredFiles": "File Ignorati",
|
||||
@@ -147,12 +149,13 @@
|
||||
"HeaderSettingsGeneral": "Generale",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSleepTimer": "Sveglia",
|
||||
"HeaderStatsLargestItems": "Largest Items",
|
||||
"HeaderStatsLargestItems": "Oggetti Grandi",
|
||||
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
||||
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
||||
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autori",
|
||||
"HeaderStatsTop5Genres": "Top 5 Generi",
|
||||
"HeaderTableOfContents": "Tabellla dei Contenuti",
|
||||
"HeaderTools": "Strumenti",
|
||||
"HeaderUpdateAccount": "Aggiorna Account",
|
||||
"HeaderUpdateAuthor": "Aggiorna Autore",
|
||||
@@ -160,13 +163,13 @@
|
||||
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
||||
"HeaderUsers": "Utenti",
|
||||
"HeaderYourStats": "Statistiche Personali",
|
||||
"LabelAbridged": "Abridged",
|
||||
"LabelAbridged": "Abbreviato",
|
||||
"LabelAccountType": "Tipo di Account",
|
||||
"LabelAccountTypeAdmin": "Admin",
|
||||
"LabelAccountTypeGuest": "Ospite",
|
||||
"LabelAccountTypeUser": "Utente",
|
||||
"LabelActivity": "Attività",
|
||||
"LabelAdded": "Added",
|
||||
"LabelAdded": "Aggiunto",
|
||||
"LabelAddedAt": "Aggiunto il",
|
||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||
@@ -191,8 +194,8 @@
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBooks": "Libri",
|
||||
"LabelChangePassword": "Cambia Password",
|
||||
"LabelChannels": "Channels",
|
||||
"LabelChapters": "Chapters",
|
||||
"LabelChannels": "Canali",
|
||||
"LabelChapters": "Capitoli",
|
||||
"LabelChaptersFound": "Capitoli Trovati",
|
||||
"LabelChapterTitle": "Titoli dei Capitoli",
|
||||
"LabelClosePlayer": "Chiudi player",
|
||||
@@ -202,7 +205,7 @@
|
||||
"LabelComplete": "Completo",
|
||||
"LabelConfirmPassword": "Conferma Password",
|
||||
"LabelContinueListening": "Continua ad Ascoltare",
|
||||
"LabelContinueReading": "Continue Reading",
|
||||
"LabelContinueReading": "Continua la Lettura",
|
||||
"LabelContinueSeries": "Continua Serie",
|
||||
"LabelCover": "Cover",
|
||||
"LabelCoverImageURL": "Cover Image URL",
|
||||
@@ -220,21 +223,24 @@
|
||||
"LabelDiscFromFilename": "Disco dal nome file",
|
||||
"LabelDiscFromMetadata": "Disco dal Metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Durata",
|
||||
"LabelDurationFound": "Durata Trovata:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Modifica",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsFromAddress": "Da Indirizzo",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Indirizzo",
|
||||
"LabelEmbeddedCover": "Cover Integrata",
|
||||
"LabelEnable": "Abilita",
|
||||
"LabelEnd": "Fine",
|
||||
"LabelEpisode": "Episodio",
|
||||
"LabelEpisodeTitle": "Titolo Episodio",
|
||||
"LabelEpisodeType": "Tipo Episodio",
|
||||
"LabelExample": "Example",
|
||||
"LabelExample": "Esempio",
|
||||
"LabelExplicit": "Esplicito",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFile": "File",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Finita",
|
||||
"LabelFolder": "Cartella",
|
||||
"LabelFolders": "Cartelle",
|
||||
"LabelFormat": "Format",
|
||||
"LabelFontScale": "Dimensione Font",
|
||||
"LabelFormat": "Formato",
|
||||
"LabelGenre": "Genere",
|
||||
"LabelGenres": "Generi",
|
||||
"LabelHardDeleteFile": "Elimina Definitivamente",
|
||||
"LabelHasEbook": "Un ebook",
|
||||
"LabelHasSupplementaryEbook": "Un ebook Supplementare",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Ora",
|
||||
"LabelIcon": "Icona",
|
||||
@@ -266,21 +275,25 @@
|
||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||
"LabelIntervalEveryHour": "Ogni ora",
|
||||
"LabelInvalidParts": "Parti Invalide",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelInvert": "Inverti",
|
||||
"LabelItem": "Oggetti",
|
||||
"LabelLanguage": "Lingua",
|
||||
"LabelLanguageDefaultServer": "Lingua di Default",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastBookAdded": "Ultimo Libro Aggiunto",
|
||||
"LabelLastBookUpdated": "Ultimo Libro Aggiornato",
|
||||
"LabelLastSeen": "Ultimi Visti",
|
||||
"LabelLastTime": "Ultima Volta",
|
||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Pagina Singola",
|
||||
"LabelLayoutSplitPage": "DIvidi Pagina",
|
||||
"LabelLess": "Poco",
|
||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||
"LabelLibrary": "Libreria",
|
||||
"LabelLibraryItem": "Elementi della Library",
|
||||
"LabelLibraryName": "Nome Libreria",
|
||||
"LabelLimit": "Limiti",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Ri-ascolta",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -295,7 +308,7 @@
|
||||
"LabelMissing": "Altro",
|
||||
"LabelMissingParts": "Parti rimantenti",
|
||||
"LabelMore": "Molto",
|
||||
"LabelMoreInfo": "More Info",
|
||||
"LabelMoreInfo": "Più Info",
|
||||
"LabelName": "Nome",
|
||||
"LabelNarrator": "Narratore",
|
||||
"LabelNarrators": "Narratori",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Nuova Password",
|
||||
"LabelNextBackupDate": "Data Prossimo Backup",
|
||||
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
||||
"LabelNoEpisodesSelected": "Nessun Episodio Selezionato",
|
||||
"LabelNotes": "Note",
|
||||
"LabelNotFinished": "Da Completare",
|
||||
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
||||
@@ -335,16 +349,19 @@
|
||||
"LabelPlayMethod": "Metodo di riproduzione",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcasts",
|
||||
"LabelPodcastType": "Timo di Podcast",
|
||||
"LabelPodcastType": "Tipo di Podcast",
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||
"LabelPrimaryEbook": "Libri Principlae",
|
||||
"LabelProgress": "Cominciati",
|
||||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Data Pubblicazione",
|
||||
"LabelPublisher": "Editore",
|
||||
"LabelPublishYear": "Anno Pubblicazione",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelRead": "Leggi",
|
||||
"LabelReadAgain": "Leggi Ancora",
|
||||
"LabelReadEbookWithoutProgress": "Leggi l'ebook senza mantenere i progressi",
|
||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||
"LabelRecentSeries": "Serie Recenti",
|
||||
"LabelRecommended": "Raccomandati",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Cerca Titolo",
|
||||
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
||||
"LabelSeason": "Stagione",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
|
||||
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
|
||||
"LabelSendEbookToDevice": "Invia ebook a...",
|
||||
"LabelSequence": "Sequenza",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Nome Serie",
|
||||
"LabelSeriesProgress": "Cominciato",
|
||||
"LabelSetEbookAsPrimary": "Immposta come Primario",
|
||||
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
||||
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di ebook a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come ebook supplementari",
|
||||
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
||||
"LabelSettingsChromecastSupport": "Supporto a Chromecast",
|
||||
"LabelSettingsDateFormat": "Formato Data",
|
||||
"LabelSettingsDisableWatcher": "Disattiva Watcher",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie",
|
||||
"LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server",
|
||||
"LabelSettingsEnableEReader": "Abilita e-reader for tutti gli Utenti",
|
||||
"LabelSettingsEnableEReaderHelp": "L'e-reader è ancora un work in progress, ma usa questa impostazione per abilitarlo a tutti i tuoi utenti (o usa lo switch \"Funzionalità sperimentali\" solo per te)",
|
||||
"LabelSettingsExperimentalFeatures": "Opzioni Sperimentali",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
|
||||
"LabelSettingsFindCovers": "Trova covers",
|
||||
"LabelSettingsFindCoversHelp": "Se il tuo audiolibro non ha una copertina incorporata o un'immagine di copertina all'interno della cartella, questa funzione tenterà di trovare una copertina.<br>Nota: aumenta il tempo di scansione",
|
||||
"LabelSettingsHideSingleBookSeries": "Nascondi una singola serie di libri",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
|
||||
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
|
||||
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
|
||||
@@ -428,8 +451,11 @@
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
|
||||
"LabelTasks": "Processi in esecuzione",
|
||||
"LabelTheme": "Tema",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Tempo di Ascolto",
|
||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||
@@ -450,7 +476,7 @@
|
||||
"LabelTracksMultiTrack": "Multi-traccia",
|
||||
"LabelTracksSingleTrack": "Traccia-singola",
|
||||
"LabelType": "Tipo",
|
||||
"LabelUnabridged": "Unabridged",
|
||||
"LabelUnabridged": "Integrale",
|
||||
"LabelUnknown": "Sconosciuto",
|
||||
"LabelUpdateCover": "Aggiornamento Cover",
|
||||
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
||||
@@ -489,16 +515,19 @@
|
||||
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
||||
"MessageCheckingCron": "Controllo cron...",
|
||||
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
||||
"MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||
@@ -506,7 +535,7 @@
|
||||
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?",
|
||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||
"MessageEmbedFinished": "Incorporamento finito!",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Fallito!",
|
||||
"MessageM4BFinished": "M4B Finito!",
|
||||
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
|
||||
"MessageMarkAllEpisodesFinished": "Segna tutti gli episodi come finiti",
|
||||
"MessageMarkAllEpisodesNotFinished": "Segna tutti gli episodi come non finiti",
|
||||
"MessageMarkAsFinished": "Segna come finito",
|
||||
"MessageMarkAsNotFinished": "Segna come da completare",
|
||||
"MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
|
||||
"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": "Rimuovi dalla coda di riproduzione",
|
||||
@@ -661,8 +691,8 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||
@@ -672,4 +702,4 @@
|
||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Huidige downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download-wachtrij",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Afleveringen",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Bestanden",
|
||||
"HeaderFindChapters": "Zoek hoofdstukken",
|
||||
"HeaderIgnoredFiles": "Genegeerde bestanden",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Recente sessies",
|
||||
"HeaderStatsTop10Authors": "Top 10 auteurs",
|
||||
"HeaderStatsTop5Genres": "Top 5 genres",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Tools",
|
||||
"HeaderUpdateAccount": "Account bijwerken",
|
||||
"HeaderUpdateAuthor": "Auteur bijwerken",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
|
||||
"LabelDiscFromMetadata": "Schijf uit metadata",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Duur",
|
||||
"LabelDurationFound": "Gevonden duur:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Wijzig",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Ingesloten cover",
|
||||
"LabelEnable": "Inschakelen",
|
||||
"LabelEnd": "Einde",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Voltooid",
|
||||
"LabelFolder": "Map",
|
||||
"LabelFolders": "Mappen",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard-delete bestand",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Uur",
|
||||
"LabelIcon": "Icoon",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Laatst gezien",
|
||||
"LabelLastTime": "Laatste keer",
|
||||
"LabelLastUpdate": "Laatste update",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Minder",
|
||||
"LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken",
|
||||
"LabelLibrary": "Bibliotheek",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limiet",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Luister opnieuw",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Nieuw wachtwoord",
|
||||
"LabelNextBackupDate": "Volgende back-up datum",
|
||||
"LabelNextScheduledRun": "Volgende geplande run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notities",
|
||||
"LabelNotFinished": "Niet Voltooid",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
|
||||
"LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Voortgang",
|
||||
"LabelProvider": "Bron",
|
||||
"LabelPubDate": "Publicatiedatum",
|
||||
"LabelPublisher": "Uitgever",
|
||||
"LabelPublishYear": "Jaar van uitgave",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Recent toegevoegd",
|
||||
"LabelRecentSeries": "Recente series",
|
||||
"LabelRecommended": "Aangeraden",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Zoek titel",
|
||||
"LabelSearchTitleOrASIN": "Zoek titel of ASIN",
|
||||
"LabelSeason": "Seizoen",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sequentie",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Naam serie",
|
||||
"LabelSeriesProgress": "Voortgang serie",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
|
||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||
"LabelSettingsDateFormat": "Datum format",
|
||||
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
|
||||
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
|
||||
"LabelSettingsEnableEReader": "E-reader inschakelen voor alle gebruikers",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader is nog in ontwikkeling, maar gebruik deze instelling om het beschikbaar te maken voor al je gebruikers (of gebruik de \"Experimentele functies\"-schakelaar voor eigen gebruik)",
|
||||
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
||||
"LabelSettingsFindCovers": "Zoek covers",
|
||||
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
|
||||
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
|
||||
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
|
||||
"LabelTasks": "Lopende taken",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Tijdsbasis",
|
||||
"LabelTimeListened": "Tijd geluisterd",
|
||||
"LabelTimeListenedToday": "Tijd geluisterd vandaag",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek",
|
||||
"MessageCheckingCron": "Cron aan het checken...",
|
||||
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
|
||||
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
||||
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
||||
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B mislukt!",
|
||||
"MessageM4BFinished": "M4B voltooid!",
|
||||
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Markeer als Voltooid",
|
||||
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
|
||||
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
|
||||
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
|
||||
"MessageRemoveAllItemsWarning": "WAARSCHUWING! Deze actie zal alle onderdelen in de bibliotheek verwijderen uit de database, inclusief enige bijwerkingen of matches die je hebt gemaakt. Dit doet niets met je onderliggende bestanden. Weet je het zeker?",
|
||||
"MessageRemoveChapter": "Verwijder hoofdstuk",
|
||||
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
|
||||
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
|
||||
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderDetails": "Szczegóły",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
"HeaderEbookFiles": "Ebook Files",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEpisodes": "Rozdziały",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader Devices",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Pliki",
|
||||
"HeaderFindChapters": "Wyszukaj rozdziały",
|
||||
"HeaderIgnoredFiles": "Zignoruj pliki",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
||||
"HeaderStatsTop10Authors": "Top 10 Autorów",
|
||||
"HeaderStatsTop5Genres": "Top 5 Gatunków",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Narzędzia",
|
||||
"HeaderUpdateAccount": "Zaktualizuj konto",
|
||||
"HeaderUpdateAuthor": "Zaktualizuj autorów",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
|
||||
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
|
||||
"LabelDownload": "Pobierz",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Czas trwania",
|
||||
"LabelDurationFound": "Znaleziona długość:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEdit": "Edytuj",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Włącz",
|
||||
"LabelEnd": "Zakończ",
|
||||
@@ -246,10 +252,13 @@
|
||||
"LabelFinished": "Zakończone",
|
||||
"LabelFolder": "Folder",
|
||||
"LabelFolders": "Foldery",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Gatunek",
|
||||
"LabelGenres": "Gatunki",
|
||||
"LabelHardDeleteFile": "Usuń trwale plik",
|
||||
"LabelHasEbook": "Has ebook",
|
||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Godzina",
|
||||
"LabelIcon": "Ikona",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "Ostatnio widziany",
|
||||
"LabelLastTime": "Ostatni czas",
|
||||
"LabelLastUpdate": "Ostatnia aktualizacja",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Mniej",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryItem": "Element biblioteki",
|
||||
"LabelLibraryName": "Nazwa biblioteki",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Słuchaj ponownie",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Informacja",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Nowe hasło",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Uwagi",
|
||||
"LabelNotFinished": "Nieukończone",
|
||||
"LabelNotificationAppriseURL": "URLe Apprise",
|
||||
@@ -339,12 +353,15 @@
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||
"LabelPrimaryEbook": "Primary ebook",
|
||||
"LabelProgress": "Postęp",
|
||||
"LabelProvider": "Dostawca",
|
||||
"LabelPubDate": "Data publikacji",
|
||||
"LabelPublisher": "Wydawca",
|
||||
"LabelPublishYear": "Rok publikacji",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||
"LabelRecentlyAdded": "Niedawno dodany",
|
||||
"LabelRecentSeries": "Ostatnie serie",
|
||||
"LabelRecommended": "Recommended",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Wyszukaj tytuł",
|
||||
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
|
||||
"LabelSeason": "Sezon",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Kolejność",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Nazwy serii",
|
||||
"LabelSeriesProgress": "Postęp w serii",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami",
|
||||
"LabelSettingsChromecastSupport": "Wsparcie Chromecast",
|
||||
"LabelSettingsDateFormat": "Format daty",
|
||||
"LabelSettingsDisableWatcher": "Wyłącz monitorowanie",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Wyłącz monitorowanie folderów dla biblioteki",
|
||||
"LabelSettingsDisableWatcherHelp": "Wyłącz automatyczne dodawanie/aktualizowanie elementów po wykryciu zmian w plikach. *Wymaga restartu serwera",
|
||||
"LabelSettingsEnableEReader": "Włącz e-czytnika dla wszystkich użytkowników",
|
||||
"LabelSettingsEnableEReaderHelp": "E-czytnik jest wciąż w fazie rozwoju, ale użyj tego ustawienia, aby udostępnić go wszystkim użytkownikom (lub użyj przełącznika \"Funkcje eksperymentalne\" aby włączyć funkcję tylko dla Ciebie)",
|
||||
"LabelSettingsExperimentalFeatures": "Funkcje eksperymentalne",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funkcje w trakcie rozwoju, które mogą zyskanć na Twojej opinii i pomocy w testowaniu. Kliknij, aby otworzyć dyskusję na githubie.",
|
||||
"LabelSettingsFindCovers": "Szukanie okładek",
|
||||
"LabelSettingsFindCoversHelp": "Jeśli audiobook nie posiada zintegrowanej okładki albo w folderze nie zostanie znaleziony plik okładki, skaner podejmie próbę pobrania okładki z sieci. <br>Uwaga: może to wydłuzyć proces skanowania",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
|
||||
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Użyj markerów Overdrive Media Markers dla rozdziałów",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTasks": "Tasks Running",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeListened": "Czas odtwarzania",
|
||||
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
|
||||
@@ -489,9 +515,12 @@
|
||||
"MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka",
|
||||
"MessageCheckingCron": "Sprawdzanie cron...",
|
||||
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się",
|
||||
"MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!",
|
||||
"MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
||||
"MessageMarkAsNotFinished": "Oznacz jako nieukończone",
|
||||
"MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"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",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"ButtonRemoveAll": "Удалить всё",
|
||||
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||
"ButtonRemoveFromContinueReading": "Удалить из Продолжить читать",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
||||
"ButtonReScan": "Пересканировать",
|
||||
"ButtonReset": "Сбросить",
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "Текущие закачки",
|
||||
"HeaderDetails": "Подробности",
|
||||
"HeaderDownloadQueue": "Очередь скачивания",
|
||||
"HeaderEbookFiles": "Файлы e-книг",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEmailSettings": "Настройки Email",
|
||||
"HeaderEpisodes": "Эпизоды",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Устройства E-книга",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderFiles": "Файлы",
|
||||
"HeaderFindChapters": "Найти главы",
|
||||
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "Последние сеансы",
|
||||
"HeaderStatsTop10Authors": "Топ 10 авторов",
|
||||
"HeaderStatsTop5Genres": "Топ 5 жанров",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTools": "Инструменты",
|
||||
"HeaderUpdateAccount": "Обновить учетную запись",
|
||||
"HeaderUpdateAuthor": "Обновить автора",
|
||||
@@ -166,7 +169,7 @@
|
||||
"LabelAccountTypeGuest": "Гость",
|
||||
"LabelAccountTypeUser": "Пользователь",
|
||||
"LabelActivity": "Активность",
|
||||
"LabelAdded": "Added",
|
||||
"LabelAdded": "Добавили",
|
||||
"LabelAddedAt": "Дата добавления",
|
||||
"LabelAddToCollection": "Добавить в коллекцию",
|
||||
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
|
||||
@@ -188,21 +191,21 @@
|
||||
"LabelBackupsMaxBackupSizeHelp": "В качестве защиты процесс бэкапирования будет завершаться ошибкой, если будет превышен настроенный размер.",
|
||||
"LabelBackupsNumberToKeep": "Сохранять бэкапов",
|
||||
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBitrate": "Битрейт",
|
||||
"LabelBooks": "Книги",
|
||||
"LabelChangePassword": "Изменить пароль",
|
||||
"LabelChannels": "Channels",
|
||||
"LabelChapters": "Chapters",
|
||||
"LabelChannels": "Каналы",
|
||||
"LabelChapters": "Главы",
|
||||
"LabelChaptersFound": "глав найдено",
|
||||
"LabelChapterTitle": "Название главы",
|
||||
"LabelClosePlayer": "Закрыть проигрыватель",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCodec": "Кодек",
|
||||
"LabelCollapseSeries": "Свернуть серии",
|
||||
"LabelCollections": "Коллекции",
|
||||
"LabelComplete": "Завершить",
|
||||
"LabelConfirmPassword": "Подтвердить пароль",
|
||||
"LabelContinueListening": "Продолжить слушать",
|
||||
"LabelContinueReading": "Continue Reading",
|
||||
"LabelContinueReading": "Продолжить читать",
|
||||
"LabelContinueSeries": "Продолжить серию",
|
||||
"LabelCover": "Обложка",
|
||||
"LabelCoverImageURL": "URL изображения обложки",
|
||||
@@ -220,15 +223,18 @@
|
||||
"LabelDiscFromFilename": "Диск из Имени файла",
|
||||
"LabelDiscFromMetadata": "Диск из Метаданных",
|
||||
"LabelDownload": "Скачать",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Длина",
|
||||
"LabelDurationFound": "Найденная длина:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbook": "E-книга",
|
||||
"LabelEbooks": "E-книги",
|
||||
"LabelEdit": "Редактировать",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEmailSettingsFromAddress": "Адрес От",
|
||||
"LabelEmailSettingsSecure": "Безопасность",
|
||||
"LabelEmailSettingsSecureHelp": "Если значение истинно, то соединение будет использовать TLS при подключении к серверу. Если значение ложно, то TLS будет использован, если сервер поддерживает расширение STARTTLS. В большинстве случаев установите это значение в истину, если вы подключаетесь к порту 465. Для порта 587 или 25 оставьте значение ложным. (из nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Тестовый адрес",
|
||||
"LabelEmbeddedCover": "Встроенная обложка",
|
||||
"LabelEnable": "Включить",
|
||||
"LabelEnd": "Конец",
|
||||
"LabelEpisode": "Эпизод",
|
||||
@@ -246,11 +252,14 @@
|
||||
"LabelFinished": "Закончен",
|
||||
"LabelFolder": "Папка",
|
||||
"LabelFolders": "Папки",
|
||||
"LabelFormat": "Format",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFormat": "Формат",
|
||||
"LabelGenre": "Жанр",
|
||||
"LabelGenres": "Жанры",
|
||||
"LabelHardDeleteFile": "Жесткое удаление файла",
|
||||
"LabelHost": "Host",
|
||||
"LabelHasEbook": "Есть e-книга",
|
||||
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
|
||||
"LabelHost": "Хост",
|
||||
"LabelHour": "Часы",
|
||||
"LabelIcon": "Иконка",
|
||||
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
||||
@@ -266,21 +275,25 @@
|
||||
"LabelIntervalEveryDay": "Каждый день",
|
||||
"LabelIntervalEveryHour": "Каждый час",
|
||||
"LabelInvalidParts": "Неверные части",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelInvert": "Инвертировать",
|
||||
"LabelItem": "Элемент",
|
||||
"LabelLanguage": "Язык",
|
||||
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastBookAdded": "Последняя книга добавлена",
|
||||
"LabelLastBookUpdated": "Последняя книга обновлена",
|
||||
"LabelLastSeen": "Последнее сканирование",
|
||||
"LabelLastTime": "Последний по времени",
|
||||
"LabelLastUpdate": "Последний обновленный",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLess": "Менее",
|
||||
"LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя",
|
||||
"LabelLibrary": "Библиотека",
|
||||
"LabelLibraryItem": "Элемент библиотеки",
|
||||
"LabelLibraryName": "Имя библиотеки",
|
||||
"LabelLimit": "Лимит",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelListenAgain": "Послушать снова",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@@ -290,12 +303,12 @@
|
||||
"LabelMediaType": "Тип медиа",
|
||||
"LabelMetadataProvider": "Провайдер",
|
||||
"LabelMetaTag": "Мета тег",
|
||||
"LabelMetaTags": "Meta Tags",
|
||||
"LabelMetaTags": "Мета теги",
|
||||
"LabelMinute": "Минуты",
|
||||
"LabelMissing": "Потеряно",
|
||||
"LabelMissingParts": "Потерянные части",
|
||||
"LabelMore": "Еще",
|
||||
"LabelMoreInfo": "More Info",
|
||||
"LabelMoreInfo": "Больше информации",
|
||||
"LabelName": "Имя",
|
||||
"LabelNarrator": "Читает",
|
||||
"LabelNarrators": "Чтецы",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "Новый пароль",
|
||||
"LabelNextBackupDate": "Следующая дата бэкапирования",
|
||||
"LabelNextScheduledRun": "Следущий запланированный запуск",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Заметки",
|
||||
"LabelNotFinished": "Не завершено",
|
||||
"LabelNotificationAppriseURL": "URL(ы) для извещений",
|
||||
@@ -336,15 +350,18 @@
|
||||
"LabelPodcast": "Подкаст",
|
||||
"LabelPodcasts": "Подкасты",
|
||||
"LabelPodcastType": "Тип подкаста",
|
||||
"LabelPort": "Port",
|
||||
"LabelPort": "Порт",
|
||||
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
||||
"LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google",
|
||||
"LabelPrimaryEbook": "Основная e-книга",
|
||||
"LabelProgress": "Прогресс",
|
||||
"LabelProvider": "Провайдер",
|
||||
"LabelPubDate": "Дата публикации",
|
||||
"LabelPublisher": "Издатель",
|
||||
"LabelPublishYear": "Год публикации",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelRead": "Читать",
|
||||
"LabelReadAgain": "Читать снова",
|
||||
"LabelReadEbookWithoutProgress": "Читать e-книгу без сохранения прогресса",
|
||||
"LabelRecentlyAdded": "Недавно добавленные",
|
||||
"LabelRecentSeries": "Последние серии",
|
||||
"LabelRecommended": "Рекомендованное",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "Поиск по названию",
|
||||
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
|
||||
"LabelSeason": "Сезон",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "Отправить e-книгу в...",
|
||||
"LabelSequence": "Последовательность",
|
||||
"LabelSeries": "Серия",
|
||||
"LabelSeriesName": "Имя серии",
|
||||
"LabelSeriesProgress": "Прогресс серии",
|
||||
"LabelSetEbookAsPrimary": "Установить как основную",
|
||||
"LabelSetEbookAsSupplementary": "Установить как дополнительную",
|
||||
"LabelSettingsAudiobooksOnly": "Только аудиокниги",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Если включить эту настройку, файлы электронных книг будут игнорироваться, за исключением случаев, когда они находятся в папке с аудиокнигами, в этом случае они будут рассматриваться как дополнительные электронные книги.",
|
||||
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
|
||||
"LabelSettingsChromecastSupport": "Поддержка Chromecast",
|
||||
"LabelSettingsDateFormat": "Формат даты",
|
||||
"LabelSettingsDisableWatcher": "Отключить отслеживание",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
|
||||
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
|
||||
"LabelSettingsEnableEReader": "Включить e-reader для всех пользователей",
|
||||
"LabelSettingsEnableEReaderHelp": "E-reader все еще находится в стадии разработки, используйте эту настройку, чтобы открыть его для всех ваших пользователей (Только для Вас используйте переключатель \"Экспериментальные Функции\")",
|
||||
"LabelSettingsExperimentalFeatures": "Экспериментальные функции",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
||||
"LabelSettingsFindCovers": "Найти обложки",
|
||||
"LabelSettingsFindCoversHelp": "Если у Ваших аудиокниг нет встроенной обложки или файла обложки в папке книги, то сканер попробует найти обложку.<br>Примечание: Это увеличит время сканирования",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
|
||||
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Overdrive Media Markers для глав",
|
||||
@@ -422,15 +445,18 @@
|
||||
"LabelStatsMinutesListening": "Минут прослушано",
|
||||
"LabelStatsOverallDays": "Всего дней",
|
||||
"LabelStatsOverallHours": "Всего часов",
|
||||
"LabelStatsWeekListening": "Недель прослушано",
|
||||
"LabelStatsWeekListening": "Прослушано за неделю",
|
||||
"LabelSubtitle": "Подзаголовок",
|
||||
"LabelSupportedFileTypes": "Поддерживаемые типы файлов",
|
||||
"LabelTag": "Тег",
|
||||
"LabelTags": "Теги",
|
||||
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя",
|
||||
"LabelTasks": "Запущенные задачи",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTimeBase": "Временная база",
|
||||
"LabelTimeListened": "Время прослушивания",
|
||||
"LabelTimeListenedToday": "Время прослушивания сегодня",
|
||||
"LabelTimeRemaining": "{0} осталось",
|
||||
@@ -489,16 +515,19 @@
|
||||
"MessageChapterStartIsAfter": "Глава начинается после окончания аудиокниги",
|
||||
"MessageCheckingCron": "Проверка cron...",
|
||||
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
|
||||
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
|
||||
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как законченные?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как незаконченные?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemoveNarrator": "Вы уверены, что хотите удалить чтеца \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Примечание: Этот жанр уже существует, поэтому они будут объединены.",
|
||||
@@ -506,7 +535,7 @@
|
||||
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
|
||||
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
|
||||
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
|
||||
"MessageDownloadingEpisode": "Эпизод скачивается",
|
||||
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
||||
"MessageEmbedFinished": "Встраивание завершено!",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Ошибка!",
|
||||
"MessageM4BFinished": "M4B Завершено!",
|
||||
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Отметить, как завершенную",
|
||||
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
||||
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "Создать плейлист из коллекции",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Подкаст не имеет URL-адреса RSS-канала, который можно использовать для поиска",
|
||||
"MessageQuickMatchDescription": "Заполняет пустые детали элемента и обложку первым результатом поиска из «{0}». Не перезаписывает сведения, если не включен параметр сервера 'Предпочитать метаданные поиска'.",
|
||||
"MessageRemoveAllItemsWarning": "ПРЕДУПРЕЖДЕНИЕ! Это действие удалит все элементы библиотеки из базы данных, включая все сделанные обновления или совпадения. Ничего не произойдет с вашими фактическими файлами. Уверены?",
|
||||
"MessageRemoveChapter": "Удалить главу",
|
||||
"MessageRemoveEpisodes": "Удалить {0} эпизод(ов)",
|
||||
"MessageRemoveFromPlayerQueue": "Удалить из очереди воспроизведения",
|
||||
@@ -661,8 +691,8 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "Элемент удален из коллекции",
|
||||
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||
"ToastSendEbookToDeviceFailed": "Не удалось отправить e-книгу на устройство",
|
||||
"ToastSendEbookToDeviceSuccess": "E-книга отправлена на устройство \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
|
||||
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
|
||||
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"ButtonRemoveAll": "移除所有",
|
||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||
"ButtonRemoveFromContinueReading": "从继续阅读中删除",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
||||
"ButtonReScan": "重新扫描",
|
||||
"ButtonReset": "重置",
|
||||
@@ -74,7 +74,7 @@
|
||||
"ButtonStartM4BEncode": "开始 M4B 编码",
|
||||
"ButtonStartMetadataEmbed": "开始嵌入元数据",
|
||||
"ButtonSubmit": "提交",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonTest": "测试",
|
||||
"ButtonUpload": "上传",
|
||||
"ButtonUploadBackup": "上传备份",
|
||||
"ButtonUploadCover": "上传封面",
|
||||
@@ -98,10 +98,12 @@
|
||||
"HeaderCurrentDownloads": "当前下载",
|
||||
"HeaderDetails": "详情",
|
||||
"HeaderDownloadQueue": "下载队列",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Settings",
|
||||
"HeaderEbookFiles": "电子书文件",
|
||||
"HeaderEmail": "邮箱",
|
||||
"HeaderEmailSettings": "邮箱设置",
|
||||
"HeaderEpisodes": "剧集",
|
||||
"HeaderEReaderDevices": "E-Reader Devices",
|
||||
"HeaderEreaderDevices": "Ereader 设备",
|
||||
"HeaderEreaderSettings": "Ereader 设置",
|
||||
"HeaderFiles": "文件",
|
||||
"HeaderFindChapters": "查找章节",
|
||||
"HeaderIgnoredFiles": "忽略的文件",
|
||||
@@ -153,6 +155,7 @@
|
||||
"HeaderStatsRecentSessions": "历史会话",
|
||||
"HeaderStatsTop10Authors": "前 10 位作者",
|
||||
"HeaderStatsTop5Genres": "前 5 种流派",
|
||||
"HeaderTableOfContents": "目录",
|
||||
"HeaderTools": "工具",
|
||||
"HeaderUpdateAccount": "更新帐户",
|
||||
"HeaderUpdateAuthor": "更新作者",
|
||||
@@ -202,7 +205,7 @@
|
||||
"LabelComplete": "已完成",
|
||||
"LabelConfirmPassword": "确认密码",
|
||||
"LabelContinueListening": "继续收听",
|
||||
"LabelContinueReading": "Continue Reading",
|
||||
"LabelContinueReading": "继续阅读",
|
||||
"LabelContinueSeries": "继续收听系列",
|
||||
"LabelCover": "封面",
|
||||
"LabelCoverImageURL": "封面图像 URL",
|
||||
@@ -220,14 +223,17 @@
|
||||
"LabelDiscFromFilename": "从文件名获取光盘",
|
||||
"LabelDiscFromMetadata": "从元数据获取光盘",
|
||||
"LabelDownload": "下载",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "持续时间",
|
||||
"LabelDurationFound": "找到持续时间:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbook": "电子书",
|
||||
"LabelEbooks": "电子书",
|
||||
"LabelEdit": "编辑",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmail": "邮箱",
|
||||
"LabelEmailSettingsFromAddress": "发件人地址",
|
||||
"LabelEmailSettingsSecure": "安全",
|
||||
"LabelEmailSettingsSecureHelp": "如果选是, 则连接将在连接到服务器时使用TLS. 如果选否, 则若服务器支持STARTTLS扩展, 则使用TLS. 在大多数情况下, 如果连接到端口465, 请将该值设置为是. 对于端口587或25, 请保持为否. (来自nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "测试地址",
|
||||
"LabelEmbeddedCover": "嵌入封面",
|
||||
"LabelEnable": "启用",
|
||||
"LabelEnd": "结束",
|
||||
@@ -246,11 +252,14 @@
|
||||
"LabelFinished": "已听完",
|
||||
"LabelFolder": "文件夹",
|
||||
"LabelFolders": "文件夹",
|
||||
"LabelFontScale": "字体比例",
|
||||
"LabelFormat": "编码格式",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
"LabelHardDeleteFile": "完全删除文件",
|
||||
"LabelHost": "Host",
|
||||
"LabelHasEbook": "有电子书",
|
||||
"LabelHasSupplementaryEbook": "有补充电子书",
|
||||
"LabelHost": "主机",
|
||||
"LabelHour": "小时",
|
||||
"LabelIcon": "图标",
|
||||
"LabelIncludeInTracklist": "包含在音轨列表中",
|
||||
@@ -275,12 +284,16 @@
|
||||
"LabelLastSeen": "上次查看时间",
|
||||
"LabelLastTime": "最近一次",
|
||||
"LabelLastUpdate": "最近更新",
|
||||
"LabelLayout": "布局",
|
||||
"LabelLayoutSinglePage": "单页",
|
||||
"LabelLayoutSplitPage": "分页",
|
||||
"LabelLess": "较少",
|
||||
"LabelLibrariesAccessibleToUser": "用户可访问的媒体库",
|
||||
"LabelLibrary": "媒体库",
|
||||
"LabelLibraryItem": "媒体库项目",
|
||||
"LabelLibraryName": "媒体库名称",
|
||||
"LabelLimit": "限制",
|
||||
"LabelLineSpacing": "行间距",
|
||||
"LabelListenAgain": "再次收听",
|
||||
"LabelLogLevelDebug": "调试",
|
||||
"LabelLogLevelInfo": "信息",
|
||||
@@ -305,6 +318,7 @@
|
||||
"LabelNewPassword": "新密码",
|
||||
"LabelNextBackupDate": "下次备份日期",
|
||||
"LabelNextScheduledRun": "下次任务运行",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "注释",
|
||||
"LabelNotFinished": "未听完",
|
||||
"LabelNotificationAppriseURL": "通知 URL(s)",
|
||||
@@ -336,15 +350,18 @@
|
||||
"LabelPodcast": "播客",
|
||||
"LabelPodcasts": "播客",
|
||||
"LabelPodcastType": "播客类型",
|
||||
"LabelPort": "Port",
|
||||
"LabelPort": "端口",
|
||||
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
||||
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
|
||||
"LabelPrimaryEbook": "主电子书",
|
||||
"LabelProgress": "进度",
|
||||
"LabelProvider": "供应商",
|
||||
"LabelPubDate": "出版日期",
|
||||
"LabelPublisher": "出版商",
|
||||
"LabelPublishYear": "发布年份",
|
||||
"LabelReadAgain": "Read Again",
|
||||
"LabelRead": "阅读",
|
||||
"LabelReadAgain": "再次阅读",
|
||||
"LabelReadEbookWithoutProgress": "阅读电子书而不保存进度",
|
||||
"LabelRecentlyAdded": "最近添加",
|
||||
"LabelRecentSeries": "最近添加系列",
|
||||
"LabelRecommended": "推荐内容",
|
||||
@@ -361,23 +378,29 @@
|
||||
"LabelSearchTitle": "搜索标题",
|
||||
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
||||
"LabelSeason": "季",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSendEbookToDevice": "发送电子书到...",
|
||||
"LabelSequence": "序列",
|
||||
"LabelSeries": "系列",
|
||||
"LabelSeriesName": "系列名称",
|
||||
"LabelSeriesProgress": "系列进度",
|
||||
"LabelSetEbookAsPrimary": "设置为主",
|
||||
"LabelSetEbookAsSupplementary": "设置为补充",
|
||||
"LabelSettingsAudiobooksOnly": "只有有声读物",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "启用此设置将忽略电子书文件, 除非它们位于有声读物文件夹中, 在这种情况下, 它们将被设置为补充电子书",
|
||||
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
|
||||
"LabelSettingsChromecastSupport": "Chromecast 支持",
|
||||
"LabelSettingsDateFormat": "日期格式",
|
||||
"LabelSettingsDisableWatcher": "禁用监视程序",
|
||||
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序",
|
||||
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
|
||||
"LabelSettingsEnableEReader": "为所有用户启用电子阅读器",
|
||||
"LabelSettingsEnableEReaderHelp": "电子阅读器仍在开发中,但可以使用此设置向所有用户打开它(或使用 \"实验功能\" 切换仅供你使用)",
|
||||
"LabelSettingsExperimentalFeatures": "实验功能",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
|
||||
"LabelSettingsFindCovers": "查找封面",
|
||||
"LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.<br>注意: 这将延长扫描时间",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
|
||||
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
|
||||
"LabelSettingsOverdriveMediaMarkers": "对章节使用 Overdrive 媒体标记",
|
||||
@@ -430,6 +453,9 @@
|
||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||
"LabelTasks": "正在运行的任务",
|
||||
"LabelTheme": "主题",
|
||||
"LabelThemeDark": "黑暗",
|
||||
"LabelThemeLight": "明亮",
|
||||
"LabelTimeBase": "时间基准",
|
||||
"LabelTimeListened": "收听时间",
|
||||
"LabelTimeListenedToday": "今日收听的时间",
|
||||
@@ -489,16 +515,19 @@
|
||||
"MessageChapterStartIsAfter": "章节开始是在有声读物结束之后",
|
||||
"MessageCheckingCron": "检查计划任务...",
|
||||
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
|
||||
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
||||
"MessageConfirmRenameGenreMergeNote": "注意: 该流派已经存在, 因此它们将被合并.",
|
||||
@@ -506,7 +535,7 @@
|
||||
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
|
||||
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
|
||||
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
|
||||
"MessageDownloadingEpisode": "正在下载剧集",
|
||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||
"MessageEmbedFinished": "嵌入完成!",
|
||||
@@ -525,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B 失败!",
|
||||
"MessageM4BFinished": "M4B 完成!",
|
||||
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "标记为已听完",
|
||||
"MessageMarkAsNotFinished": "标记为未听完",
|
||||
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||
@@ -565,7 +596,6 @@
|
||||
"MessagePlaylistCreateFromCollection": "从收藏中创建播放列表",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
|
||||
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
|
||||
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的媒体库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
|
||||
"MessageRemoveChapter": "移除章节",
|
||||
"MessageRemoveEpisodes": "移除 {0} 剧集",
|
||||
"MessageRemoveFromPlayerQueue": "从播放队列中移除",
|
||||
@@ -661,8 +691,8 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to send Ebook to device",
|
||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||
"ToastSendEbookToDeviceFailed": "发送电子书到设备失败",
|
||||
"ToastSendEbookToDeviceSuccess": "电子书已经发送到设备 \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "更新系列失败",
|
||||
"ToastSeriesUpdateSuccess": "系列已更新",
|
||||
"ToastSessionDeleteFailed": "删除会话失败",
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
This is an example of a fully expanded book library item
|
||||
*/
|
||||
|
||||
const LibraryItem = require('../server/objects/LibraryItem')
|
||||
|
||||
new LibraryItem({
|
||||
id: 'li_abai123wir',
|
||||
ino: "55450570412017066",
|
||||
libraryId: 'lib_1239p1d8',
|
||||
folderId: 'fol_192ab8901',
|
||||
path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule',
|
||||
relPath: '/Terry Goodkind/Sword of Truth/1 - Wizards First Rule',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127,
|
||||
lastScan: 1646784672127,
|
||||
scanVersion: 1.72,
|
||||
isMissing: false,
|
||||
isInvalid: false,
|
||||
mediaType: 'book',
|
||||
media: { // Book.js
|
||||
coverPath: '/metadata/items/li_abai123wir/cover.webp',
|
||||
tags: ['favorites'],
|
||||
lastCoverSearch: null,
|
||||
lastCoverSearchQuery: null,
|
||||
metadata: { // BookMetadata.js
|
||||
title: 'Wizards First Rule',
|
||||
subtitle: null,
|
||||
authors: [
|
||||
{
|
||||
id: 'au_42908lkajsfdk',
|
||||
name: 'Terry Goodkind'
|
||||
}
|
||||
],
|
||||
narrators: ['Sam Tsoutsouvas'],
|
||||
series: [
|
||||
{
|
||||
id: 'se_902384lansf',
|
||||
name: 'Sword of Truth',
|
||||
sequence: 1
|
||||
}
|
||||
],
|
||||
genres: ['Fantasy', 'Adventure'],
|
||||
publishedYear: '1994',
|
||||
publishedDate: '1994-01-01',
|
||||
publisher: 'Brilliance Audio',
|
||||
description: 'In the aftermath of the brutal murder of his father, a mysterious woman...',
|
||||
isbn: '289374092834',
|
||||
asin: '19023819203',
|
||||
language: 'english',
|
||||
explicit: false
|
||||
},
|
||||
audioFiles: [
|
||||
{ // AudioFile.js
|
||||
ino: "55450570412017066",
|
||||
index: 1,
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'audiofile.mp3',
|
||||
ext: '.mp3',
|
||||
path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/CD01/audiofile.mp3',
|
||||
relPath: '/CD01/audiofile.mp3',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
trackNumFromMeta: 1,
|
||||
discNumFromMeta: null,
|
||||
trackNumFromFilename: null,
|
||||
discNumFromFilename: 1,
|
||||
manuallyVerified: false,
|
||||
exclude: false,
|
||||
invalid: false,
|
||||
format: "MP2/3 (MPEG audio layer 2/3)",
|
||||
duration: 2342342,
|
||||
bitRate: 324234,
|
||||
language: null,
|
||||
codec: 'mp3',
|
||||
timeBase: "1/14112000",
|
||||
channels: 1,
|
||||
channelLayout: "mono",
|
||||
chapters: [],
|
||||
embeddedCoverArt: 'jpeg', // Video stream codec ['mjpeg', 'jpeg', 'png'] or null
|
||||
metaTags: { // AudioMetaTags.js
|
||||
tagAlbum: '',
|
||||
tagArtist: '',
|
||||
tagGenre: '',
|
||||
tagTitle: '',
|
||||
tagSeries: '',
|
||||
tagSeriesPart: '',
|
||||
tagTrack: '',
|
||||
tagDisc: '',
|
||||
tagSubtitle: '',
|
||||
tagAlbumArtist: '',
|
||||
tagDate: '',
|
||||
tagComposer: '',
|
||||
tagPublisher: '',
|
||||
tagComment: '',
|
||||
tagDescription: '',
|
||||
tagEncoder: '',
|
||||
tagEncodedBy: '',
|
||||
tagIsbn: '',
|
||||
tagLanguage: '',
|
||||
tagASIN: ''
|
||||
},
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
}
|
||||
],
|
||||
chapters: [
|
||||
{
|
||||
id: 0,
|
||||
title: 'Chapter 01',
|
||||
start: 0,
|
||||
end: 2467.753
|
||||
}
|
||||
],
|
||||
missingParts: [4, 10], // Array of missing parts in tracklist
|
||||
ebookFile: { // EBookFile.js
|
||||
ino: "55450570412017066",
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'ebookfile.mobi',
|
||||
ext: '.mobi',
|
||||
path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/ebookfile.mobi',
|
||||
relPath: '/ebookfile.mobi',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
ebookFormat: 'mobi',
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
}
|
||||
},
|
||||
libraryFiles: [
|
||||
{ // LibraryFile.js
|
||||
ino: "55450570412017066",
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'cover.png',
|
||||
ext: '.png',
|
||||
path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/subfolder/cover.png',
|
||||
relPath: '/subfolder/cover.png',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
},
|
||||
{ // LibraryFile.js
|
||||
ino: "55450570412017066",
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'cover.png',
|
||||
ext: '.mobi',
|
||||
path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/ebookfile.mobi',
|
||||
relPath: '/ebookfile.mobi',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
This is an example of a fully expanded podcast library item (under construction)
|
||||
*/
|
||||
|
||||
const LibraryItem = require('../server/objects/LibraryItem')
|
||||
|
||||
new LibraryItem({
|
||||
id: 'li_abai123wir',
|
||||
ino: "55450570412017066",
|
||||
libraryId: 'lib_1239p1d8',
|
||||
folderId: 'fol_192ab8901',
|
||||
path: '/podcasts/Great Podcast Name',
|
||||
relPath: '/Great Podcast Name',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127,
|
||||
lastScan: 1646784672127,
|
||||
scanVersion: 1.72,
|
||||
isMissing: false,
|
||||
isInvalid: false,
|
||||
mediaType: 'podcast',
|
||||
media: { // Podcast.js
|
||||
coverPath: '/metadata/items/li_abai123wir/cover.webp',
|
||||
tags: ['favorites'],
|
||||
lastCoverSearch: null,
|
||||
lastCoverSearchQuery: null,
|
||||
metadata: { // PodcastMetadata.js
|
||||
title: 'Great Podcast Name',
|
||||
artist: 'Some Artist Name',
|
||||
genres: ['Fantasy', 'Adventure'],
|
||||
publishedDate: '1994-01-01',
|
||||
description: 'In the aftermath of the brutal murder of his father, a mysterious woman...',
|
||||
feedUrl: '',
|
||||
itunesPageUrl: '',
|
||||
itunesId: '',
|
||||
itunesArtistId: '',
|
||||
explicit: false
|
||||
},
|
||||
episodes: [
|
||||
{ // PodcastEpisode.js
|
||||
id: 'ep_289374asf0a98',
|
||||
index: 1,
|
||||
// TODO: podcast episode data and PodcastEpisodeMetadata
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
}
|
||||
]
|
||||
},
|
||||
libraryFiles: [
|
||||
{ // LibraryFile.js
|
||||
ino: "55450570412017066",
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'cover.png',
|
||||
ext: '.png',
|
||||
path: '/podcasts/Great Podcast Name/cover.png',
|
||||
relPath: '/cover.png',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
},
|
||||
{ // LibraryFile.js
|
||||
ino: "55450570412017066",
|
||||
metadata: { // FileMetadata.js
|
||||
filename: 'episode_1.mp3',
|
||||
ext: '.mp3',
|
||||
path: '/podcasts/Great Podcast Name/episode_1.mp3',
|
||||
relPath: '/episode_1.mp3',
|
||||
mtimeMs: 1646784672127,
|
||||
ctimeMs: 1646784672127,
|
||||
birthtimeMs: 1646784672127,
|
||||
size: 1197449516
|
||||
},
|
||||
addedAt: 1646784672127,
|
||||
updatedAt: 1646784672127
|
||||
}
|
||||
]
|
||||
})
|
||||
2380
package-lock.json
generated
2380
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.22",
|
||||
"version": "2.3.3",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"client/dist/**/*",
|
||||
"server/Db.js"
|
||||
"node_modules/sqlite3/lib/binding/**/*.node"
|
||||
],
|
||||
"scripts": [
|
||||
"prod.js",
|
||||
@@ -36,7 +36,9 @@
|
||||
"htmlparser2": "^8.0.1",
|
||||
"node-tone": "^1.0.1",
|
||||
"nodemailer": "^6.9.2",
|
||||
"sequelize": "^6.32.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"sqlite3": "^5.1.6",
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
30
readme.md
30
readme.md
@@ -32,7 +32,7 @@ Audiobookshelf is a self-hosted audiobook and podcast server.
|
||||
* Chapter editor and chapter lookup (using [Audnexus API](https://audnex.us/))
|
||||
* Merge your audio files into a single m4b
|
||||
* Embed metadata and cover image into your audio files (using [Tone](https://github.com/sandreas/tone))
|
||||
* Basic ebook support and e-reader *(experimental)*
|
||||
* Basic ebook support and ereader
|
||||
|
||||
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
||||
|
||||
@@ -117,9 +117,11 @@ Add this to the site config file on your Apache server after you have changed th
|
||||
|
||||
For this to work you must enable at least the following mods using `a2enmod`:
|
||||
- `ssl`
|
||||
- `proxy_module`
|
||||
- `proxy_wstunnel_module`
|
||||
- `rewrite_module`
|
||||
- `proxy`
|
||||
- `proxy_http`
|
||||
- `proxy_balancer`
|
||||
- `proxy_wstunnel`
|
||||
- `rewrite`
|
||||
|
||||
```bash
|
||||
<IfModule mod_ssl.c>
|
||||
@@ -144,6 +146,26 @@ For this to work you must enable at least the following mods using `a2enmod`:
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
Some SSL certificates like those signed by Let's Encrypt require ACME validation. To allow Let's Encrypt to write and confirm
|
||||
the ACME challenge, edit your VirtualHost definition to prevent proxying traffic that queries `/.well-known` and instead
|
||||
serve that directly:
|
||||
```bash
|
||||
<VirtualHost *:443>
|
||||
# ...
|
||||
|
||||
# create the directory structure /.well-known/acme-challenges
|
||||
# within DocumentRoot and give the HTTP user recursive write
|
||||
# access to it.
|
||||
DocumentRoot /path/to/local/directory
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass /.well-known !
|
||||
ProxyPass / http://localhost:<audiobookshelf_port>/
|
||||
|
||||
# ...
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
|
||||
### SWAG Reverse Proxy
|
||||
|
||||
|
||||
@@ -2,21 +2,10 @@ const bcrypt = require('./libs/bcryptjs')
|
||||
const jwt = require('./libs/jsonwebtoken')
|
||||
const requestIp = require('./libs/requestIp')
|
||||
const Logger = require('./Logger')
|
||||
const Database = require('./Database')
|
||||
|
||||
class Auth {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
|
||||
this.user = null
|
||||
}
|
||||
|
||||
get username() {
|
||||
return this.user ? this.user.username : 'nobody'
|
||||
}
|
||||
|
||||
get users() {
|
||||
return this.db.users
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
cors(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
@@ -35,20 +24,20 @@ class Auth {
|
||||
async initTokenSecret() {
|
||||
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
||||
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
||||
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||
} else {
|
||||
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
||||
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||
}
|
||||
await this.db.updateServerSettings()
|
||||
await Database.updateServerSettings()
|
||||
|
||||
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||
if (this.db.users.length) {
|
||||
for (const user of this.db.users) {
|
||||
if (Database.users.length) {
|
||||
for (const user of Database.users) {
|
||||
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
||||
}
|
||||
await this.db.updateEntities('user', this.db.users)
|
||||
await Database.updateBulkUsers(Database.users)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +57,7 @@ class Auth {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
var user = await this.verifyToken(token)
|
||||
const user = await this.verifyToken(token)
|
||||
if (!user) {
|
||||
Logger.error('Verify Token User Not Found', token)
|
||||
return res.sendStatus(404)
|
||||
@@ -95,7 +84,7 @@ class Auth {
|
||||
}
|
||||
|
||||
generateAccessToken(payload) {
|
||||
return jwt.sign(payload, global.ServerSettings.tokenSecret);
|
||||
return jwt.sign(payload, Database.serverSettings.tokenSecret)
|
||||
}
|
||||
|
||||
authenticateUser(token) {
|
||||
@@ -104,12 +93,12 @@ class Auth {
|
||||
|
||||
verifyToken(token) {
|
||||
return new Promise((resolve) => {
|
||||
jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => {
|
||||
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
|
||||
if (!payload || err) {
|
||||
Logger.error('JWT Verify Token Failed', err)
|
||||
return resolve(null)
|
||||
}
|
||||
const user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||
const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username)
|
||||
resolve(user || null)
|
||||
})
|
||||
})
|
||||
@@ -118,9 +107,9 @@ class Auth {
|
||||
getUserLoginResponsePayload(user) {
|
||||
return {
|
||||
user: user.toJSONForBrowser(),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||
ereaderDevices: this.db.emailSettings.getEReaderDevices(user),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||
Source: global.Source
|
||||
}
|
||||
}
|
||||
@@ -130,7 +119,7 @@ class Auth {
|
||||
const username = (req.body.username || '').toLowerCase()
|
||||
const password = req.body.password || ''
|
||||
|
||||
const user = this.users.find(u => u.username.toLowerCase() === username)
|
||||
const user = Database.users.find(u => u.username.toLowerCase() === username)
|
||||
|
||||
if (!user?.isActive) {
|
||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||
@@ -142,7 +131,7 @@ class Auth {
|
||||
}
|
||||
|
||||
// Check passwordless root user
|
||||
if (user.id === 'root' && (!user.pash || user.pash === '')) {
|
||||
if (user.type === 'root' && (!user.pash || user.pash === '')) {
|
||||
if (password) {
|
||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||
} else {
|
||||
@@ -166,15 +155,6 @@ class Auth {
|
||||
}
|
||||
}
|
||||
|
||||
// Not in use now
|
||||
lockUser(user) {
|
||||
user.isLocked = true
|
||||
return this.db.updateEntity('user', user).catch((error) => {
|
||||
Logger.error('[Auth] Failed to lock user', user.username, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
comparePassword(password, user) {
|
||||
if (user.type === 'root' && !password && !user.pash) return true
|
||||
if (!password || !user.pash) return false
|
||||
@@ -184,7 +164,7 @@ class Auth {
|
||||
async userChangePassword(req, res) {
|
||||
var { password, newPassword } = req.body
|
||||
newPassword = newPassword || ''
|
||||
var matchingUser = this.users.find(u => u.id === req.user.id)
|
||||
const matchingUser = Database.users.find(u => u.id === req.user.id)
|
||||
|
||||
// Only root can have an empty password
|
||||
if (matchingUser.type !== 'root' && !newPassword) {
|
||||
@@ -193,14 +173,14 @@ class Auth {
|
||||
})
|
||||
}
|
||||
|
||||
var compare = await this.comparePassword(password, matchingUser)
|
||||
const compare = await this.comparePassword(password, matchingUser)
|
||||
if (!compare) {
|
||||
return res.json({
|
||||
error: 'Invalid password'
|
||||
})
|
||||
}
|
||||
|
||||
var pw = ''
|
||||
let pw = ''
|
||||
if (newPassword) {
|
||||
pw = await this.hashPass(newPassword)
|
||||
if (!pw) {
|
||||
@@ -211,7 +191,8 @@ class Auth {
|
||||
}
|
||||
|
||||
matchingUser.pash = pw
|
||||
var success = await this.db.updateEntity('user', matchingUser)
|
||||
|
||||
const success = await Database.updateUser(matchingUser)
|
||||
if (success) {
|
||||
res.json({
|
||||
success: true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user