mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-03 17:30:39 +02:00
Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5d334874 | |||
| 6822628994 | |||
| 98d9fd8c32 | |||
| e2cca60853 | |||
| e80b313a7b | |||
| b09b95ef24 | |||
| aec45d04f7 | |||
| 87d037cb0a | |||
| f6baf06164 | |||
| 7e75845851 | |||
| 2a11932822 | |||
| 80fee92037 | |||
| d0c02a801a | |||
| 9e13c64408 | |||
| 826963bf00 | |||
| 39b6ede1e9 | |||
| 066d853156 | |||
| efae529fac | |||
| 934c0b9093 | |||
| f02992dd4d | |||
| 10011bd6a3 | |||
| a44ee913c4 | |||
| adccccbd7a | |||
| 05b1b2be36 | |||
| 7cc35a2cbe | |||
| 8d479b6e34 | |||
| 74d300f048 | |||
| 1dd1fe8994 | |||
| 03115e5e53 | |||
| b1c07834be | |||
| b9da3fa30e | |||
| 42ff3d8314 | |||
| e63aab95d8 | |||
| 9123dcb365 | |||
| 7567e91878 | |||
| 1b1bdea3c8 | |||
| 2df95c1712 | |||
| 4ad1cd2968 | |||
| 0ecfdab463 | |||
| 75276f5a44 | |||
| 4585d2816b | |||
| f8f94f2a6d | |||
| 2c8448d147 | |||
| ea1d051cfb | |||
| a38e43213d | |||
| 6cac8fcd6e | |||
| 8e65c78869 | |||
| a3899b68e1 | |||
| 1187f91063 | |||
| 7c288a5ff9 | |||
| e0dae44c7d | |||
| 754498958d | |||
| ec15978e26 | |||
| 469167df66 | |||
| e7c43a3f32 | |||
| 24989e73ae | |||
| 13427b9f70 | |||
| adafefecd4 | |||
| 6f96b069b5 | |||
| 6c1b4e3a36 | |||
| 21343ffbd1 | |||
| 4f94deefa0 | |||
| 332078e6c1 | |||
| ff0d6326d3 | |||
| 8d451217a3 | |||
| f21d69339f | |||
| c77cead9ae | |||
| b334d40998 | |||
| 4e4a976050 | |||
| 9d7d4c6902 | |||
| 7222171c5b | |||
| 361732a463 | |||
| 1ebe8a6f4c | |||
| a98942a361 | |||
| 0bc89cd40f | |||
| 2ae86ab5bb | |||
| c707bcf0f6 | |||
| 10040ba9fa | |||
| 7afda1295b | |||
| 6d6e8613cf | |||
| 3651fffbee | |||
| 8d03b23f46 | |||
| fc44c801f2 | |||
| 6056c14926 | |||
| f465193b9c | |||
| 09c9c28028 | |||
| f1130eb63a | |||
| db80cec168 | |||
| 38029d1202 | |||
| aac2879652 | |||
| 8c9fc3ddb5 | |||
| 33e04d0cbb | |||
| fbb5fd41fb | |||
| 43a5296dd7 | |||
| 345ff1aa66 | |||
| 56e3449db6 | |||
| 1372c24535 | |||
| 409c5f7b75 | |||
| 83d0db0607 | |||
| 91b6c4412d | |||
| 09eefae808 | |||
| 80b3bfea51 | |||
| 516298b5b2 | |||
| 8edab98163 | |||
| 58da095bcf | |||
| b9633691f4 | |||
| 7ec1d8ee5f | |||
| 83a1374e79 | |||
| 5ef00bac92 | |||
| 95c4b3862b | |||
| eeaf012cdc | |||
| 11120a3765 | |||
| 4d0acb30ba | |||
| 4dbe8d29d9 | |||
| 0ca4ff4fca | |||
| 8be1651c6b | |||
| af2db86d1a | |||
| 57c834f88d | |||
| 65fdebde20 | |||
| b58e42ebf3 | |||
| b2d45f598b | |||
| 09c4e690c6 | |||
| 67ba481dca | |||
| 710a62c2af | |||
| 5a9eed0a5a | |||
| 354e16e462 | |||
| 1d974375a0 | |||
| 1c40af3eef | |||
| daa8c4cd67 | |||
| d5da4441cd | |||
| 80aea0c82d | |||
| 14836eeb0d | |||
| 85e9883d3e | |||
| 80ca73e491 | |||
| 22323f606d | |||
| 01b65eb678 | |||
| d1d94c37a7 | |||
| 838a24c8a5 | |||
| 3f380b0839 | |||
| 7fdf1a1d7f | |||
| c2793fe29b | |||
| 38596d017f | |||
| 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 |
@@ -15,3 +15,4 @@ test/
|
|||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
.idea/*
|
||||||
|
|||||||
+8
-6
@@ -10,11 +10,15 @@ FROM sandreas/tone:v0.1.5 AS tone
|
|||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
curl \
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg
|
ffmpeg \
|
||||||
|
make \
|
||||||
|
python3 \
|
||||||
|
g++
|
||||||
|
|
||||||
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
||||||
COPY --from=build /client/dist /client/dist
|
COPY --from=build /client/dist /client/dist
|
||||||
@@ -23,10 +27,8 @@ COPY server server
|
|||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
RUN apk del make python3 g++
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
HEALTHCHECK \
|
|
||||||
--interval=30s \
|
|
||||||
--timeout=3s \
|
|
||||||
--start-period=10s \
|
|
||||||
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -60,13 +60,13 @@ install_ffmpeg() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
$WGET
|
$WGET
|
||||||
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1
|
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner
|
||||||
rm ffmpeg-git-amd64-static.tar.xz
|
rm ffmpeg-git-amd64-static.tar.xz
|
||||||
|
|
||||||
# Temp downloading tone library to the ffmpeg dir
|
# Temp downloading tone library to the ffmpeg dir
|
||||||
echo "Getting tone.."
|
echo "Getting tone.."
|
||||||
$WGET_TONE
|
$WGET_TONE
|
||||||
tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1
|
tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1 --no-same-owner
|
||||||
rm tone-0.1.5-linux-x64.tar.gz
|
rm tone-0.1.5-linux-x64.tar.gz
|
||||||
|
|
||||||
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<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>
|
</nuxt-link>
|
||||||
|
|
||||||
<ui-libraries-dropdown class="mr-2" />
|
<ui-libraries-dropdown class="mr-2" />
|
||||||
@@ -149,9 +149,6 @@ export default {
|
|||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.$store.state.processingBatch
|
return this.$store.state.processingBatch
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
isChromecastEnabled() {
|
isChromecastEnabled() {
|
||||||
return this.$store.getters['getServerSetting']('chromecastEnabled')
|
return this.$store.getters['getServerSetting']('chromecastEnabled')
|
||||||
},
|
},
|
||||||
@@ -306,13 +303,13 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch update success!')
|
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch update failed')
|
this.$toast.error(this.$strings.ToastBatchUpdateFailed)
|
||||||
console.error('Failed to batch update read/not read', error)
|
console.error('Failed to batch update read/not read', error)
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
libraryName() {
|
libraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
@@ -171,7 +171,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
const categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
@@ -349,8 +349,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
episodeAdded(episodeWithLibraryItem) {
|
episodeAdded(episodeWithLibraryItem) {
|
||||||
console.log('Podcast episode added', episodeWithLibraryItem)
|
|
||||||
|
|
||||||
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
|
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
|
||||||
if (!this.search && isThisLibrary) {
|
if (!this.search && isThisLibrary) {
|
||||||
this.fetchCategories()
|
this.fetchCategories()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<!-- issues page remove all button -->
|
<!-- 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-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>
|
</template>
|
||||||
<!-- search page -->
|
<!-- search page -->
|
||||||
<template v-else-if="page === 'search'">
|
<template v-else-if="page === 'search'">
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ export default {
|
|||||||
id: 'config-item-metadata-utils',
|
id: 'config-item-metadata-utils',
|
||||||
title: this.$strings.HeaderItemMetadataUtils,
|
title: this.$strings.HeaderItemMetadataUtils,
|
||||||
path: '/config/item-metadata-utils'
|
path: '/config/item-metadata-utils'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-rss-feeds',
|
||||||
|
title: this.$strings.HeaderRSSFeeds,
|
||||||
|
path: '/config/rss-feeds'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,6 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
libraryMediaType() {
|
libraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
@@ -316,12 +313,12 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&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) => {
|
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
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -626,6 +623,11 @@ export default {
|
|||||||
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
||||||
},
|
},
|
||||||
async init(bookshelf) {
|
async init(bookshelf) {
|
||||||
|
if (this.entityName === 'series') {
|
||||||
|
this.booksPerFetch = 50
|
||||||
|
} else {
|
||||||
|
this.booksPerFetch = 100
|
||||||
|
}
|
||||||
this.checkUpdateSearchParams()
|
this.checkUpdateSearchParams()
|
||||||
this.initSizeData(bookshelf)
|
this.initSizeData(bookshelf)
|
||||||
|
|
||||||
|
|||||||
@@ -116,9 +116,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- 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>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -174,12 +179,6 @@ export default {
|
|||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.state.serverSettings.dateFormat
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
enableEReader() {
|
|
||||||
return this.store.getters['getServerSetting']('enableEReader')
|
|
||||||
},
|
|
||||||
_libraryItem() {
|
_libraryItem() {
|
||||||
return this.libraryItem || {}
|
return this.libraryItem || {}
|
||||||
},
|
},
|
||||||
@@ -220,7 +219,7 @@ export default {
|
|||||||
return this.mediaMetadata.series
|
return this.mediaMetadata.series
|
||||||
},
|
},
|
||||||
seriesSequence() {
|
seriesSequence() {
|
||||||
return this.series ? this.series.sequence : null
|
return this.series?.sequence || null
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
@@ -233,9 +232,11 @@ export default {
|
|||||||
return this.media.numTracks || 0 // toJSONMinified
|
return this.media.numTracks || 0 // toJSONMinified
|
||||||
},
|
},
|
||||||
numEpisodes() {
|
numEpisodes() {
|
||||||
if (!this.isPodcast) return 0
|
|
||||||
return this.media.numEpisodes || 0
|
return this.media.numEpisodes || 0
|
||||||
},
|
},
|
||||||
|
numEpisodesIncomplete() {
|
||||||
|
return this._libraryItem.numEpisodesIncomplete || 0
|
||||||
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
@@ -317,6 +318,7 @@ export default {
|
|||||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||||
|
if (this.orderBy === 'media.metadata.publishedYear' && this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
episodeProgress() {
|
episodeProgress() {
|
||||||
@@ -367,13 +369,13 @@ export default {
|
|||||||
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && this.ebookFormat
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this._libraryItem.isMissing
|
return this._libraryItem.isMissing
|
||||||
@@ -686,7 +688,6 @@ export default {
|
|||||||
.$patch(apiEndpoint, updatePayload)
|
.$patch(apiEndpoint, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
@@ -763,6 +764,8 @@ export default {
|
|||||||
this.store.commit('globals/setConfirmPrompt', payload)
|
this.store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
removeSeriesFromContinueListening() {
|
removeSeriesFromContinueListening() {
|
||||||
|
if (!this.series) return
|
||||||
|
|
||||||
const axios = this.$axios || this.$nuxt.$axios
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
this.processing = true
|
this.processing = true
|
||||||
axios
|
axios
|
||||||
@@ -888,7 +891,7 @@ export default {
|
|||||||
var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
|
var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
|
||||||
var el = instance.$el
|
var el = instance.$el
|
||||||
|
|
||||||
var elHeight = this.moreMenuItems.length * 28 + 2
|
var elHeight = this.moreMenuItems.length * 28 + 10
|
||||||
var elWidth = 130
|
var elWidth = 130
|
||||||
|
|
||||||
var bottomOfIcon = wrapperBox.top + wrapperBox.height
|
var bottomOfIcon = wrapperBox.top + wrapperBox.height
|
||||||
@@ -921,7 +924,7 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
this.store.commit('showEReader', libraryItem)
|
this.store.commit('showEReader', { libraryItem, keepProgress: true })
|
||||||
},
|
},
|
||||||
selectBtnClick(evt) {
|
selectBtnClick(evt) {
|
||||||
if (this.processingBatch) return
|
if (this.processingBatch) return
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default {
|
|||||||
return this.narrator?.name || ''
|
return this.narrator?.name || ''
|
||||||
},
|
},
|
||||||
numBooks() {
|
numBooks() {
|
||||||
return this.narrator?.books?.length || 0
|
return this.narrator?.numBooks || this.narrator?.books?.length || 0
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="publishedYear" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -20,15 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="publisher" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ publisher }}
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicAlbum" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="podcastType" class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="capitalize">
|
<div class="capitalize">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5" v-if="genres.length">
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5" v-if="tags.length">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs">{{ selectedText }}</span>
|
<span class="block truncate text-xs">{{ selectedText }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -14,12 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</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">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :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">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
totalResults() {
|
totalResults() {
|
||||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length
|
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
</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" />
|
<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>
|
</svg>
|
||||||
</span>
|
</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>
|
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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">
|
<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">
|
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in selectItems">
|
<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">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons text-2xl">arrow_right</span>
|
<span class="material-icons text-2xl">arrow_right</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
||||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons text-2xl">arrow_left</span>
|
<span class="material-icons text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate">Back</span>
|
<span class="font-normal block truncate">Back</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
<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>
|
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('no-series'))">
|
|
||||||
<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">
|
<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">
|
<div class="flex items-center">
|
||||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -72,9 +75,8 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showMenu(newVal) {
|
showMenu(newVal) {
|
||||||
if (!newVal) {
|
if (newVal) {
|
||||||
if (this.sublist && !this.selectedItemSublist) this.sublist = null
|
this.sublist = this.selectedItemSublist
|
||||||
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -122,6 +124,11 @@ export default {
|
|||||||
value: 'narrators',
|
value: 'narrators',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublisher,
|
||||||
|
value: 'publishers',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
value: 'languages',
|
value: 'languages',
|
||||||
@@ -165,6 +172,11 @@ export default {
|
|||||||
value: 'narrators',
|
value: 'narrators',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublisher,
|
||||||
|
value: 'publishers',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
value: 'languages',
|
value: 'languages',
|
||||||
@@ -186,9 +198,9 @@ export default {
|
|||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelEbook,
|
text: this.$strings.LabelEbooks,
|
||||||
value: 'ebook',
|
value: 'ebooks',
|
||||||
sublist: false
|
sublist: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelAbridged,
|
text: this.$strings.LabelAbridged,
|
||||||
@@ -260,21 +272,25 @@ export default {
|
|||||||
return this.bookItems
|
return this.bookItems
|
||||||
},
|
},
|
||||||
selectedItemSublist() {
|
selectedItemSublist() {
|
||||||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
return this.selected?.includes('.') ? this.selected.split('.')[0] : null
|
||||||
},
|
},
|
||||||
selectedText() {
|
selectedText() {
|
||||||
if (!this.selected) return ''
|
if (!this.selected) return ''
|
||||||
var parts = this.selected.split('.')
|
const parts = this.selected.split('.')
|
||||||
var filterName = this.selectItems.find((i) => i.value === parts[0])
|
const filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||||
var filterValue = null
|
let filterValue = null
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
var decoded = this.$decode(parts[1])
|
const decoded = this.$decode(parts[1])
|
||||||
if (decoded.startsWith('aut_')) {
|
if (parts[0] === 'authors') {
|
||||||
var author = this.authors.find((au) => au.id == decoded)
|
const author = this.authors.find((au) => au.id == decoded)
|
||||||
if (author) filterValue = author.name
|
if (author) filterValue = author.name
|
||||||
} else if (decoded.startsWith('ser_')) {
|
} else if (parts[0] === 'series') {
|
||||||
var series = this.series.find((se) => se.id == decoded)
|
if (decoded === 'no-series') {
|
||||||
if (series) filterValue = series.name
|
filterValue = this.$strings.MessageNoSeries
|
||||||
|
} else {
|
||||||
|
const series = this.series.find((se) => se.id == decoded)
|
||||||
|
if (series) filterValue = series.name
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
filterValue = decoded
|
filterValue = decoded
|
||||||
}
|
}
|
||||||
@@ -307,6 +323,9 @@ export default {
|
|||||||
languages() {
|
languages() {
|
||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
|
publishers() {
|
||||||
|
return this.filterData.publishers || []
|
||||||
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -329,6 +348,10 @@ export default {
|
|||||||
},
|
},
|
||||||
tracks() {
|
tracks() {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
id: 'none',
|
||||||
|
name: this.$strings.LabelTracksNone
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'single',
|
id: 'single',
|
||||||
name: this.$strings.LabelTracksSingleTrack
|
name: this.$strings.LabelTracksSingleTrack
|
||||||
@@ -339,6 +362,18 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
ebooks() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ebook',
|
||||||
|
name: this.$strings.LabelHasEbook
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'supplementary',
|
||||||
|
name: this.$strings.LabelHasSupplementaryEbook
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -396,7 +431,7 @@ export default {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
const sublistItems = (this[this.sublist] || []).map((item) => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
return {
|
return {
|
||||||
text: item,
|
text: item,
|
||||||
@@ -409,6 +444,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (this.sublist === 'series') {
|
||||||
|
sublistItems.unshift({
|
||||||
|
text: this.$strings.MessageNoSeries,
|
||||||
|
value: this.$encode('no-series')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sublistItems
|
||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
@@ -433,7 +475,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var val = option.value
|
const val = option.value
|
||||||
if (this.selected === val) {
|
if (this.selected === val) {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</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">
|
<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">
|
<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>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<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>
|
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<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 class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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">
|
<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">
|
<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>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<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>
|
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img v-if="width > 100" src="/Logo.png" class="mb-2" :style="{ height: 40 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-center text-error" :style="{ fontSize: invalidCoverFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -58,6 +58,9 @@ export default {
|
|||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.width / 120
|
return this.width / 120
|
||||||
},
|
},
|
||||||
|
invalidCoverFontSize() {
|
||||||
|
return Math.max(this.sizeMultiplier * 0.8, 0.5)
|
||||||
|
},
|
||||||
placeholderCoverPadding() {
|
placeholderCoverPadding() {
|
||||||
return 0.8 * this.sizeMultiplier
|
return 0.8 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<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="w-full p-8">
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<div class="w-1/2 px-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" />
|
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -353,7 +352,8 @@ export default {
|
|||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false
|
||||||
},
|
},
|
||||||
librariesAccessible: []
|
librariesAccessible: [],
|
||||||
|
itemTagsSelected: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="audiofile-data-modal" :width="700" :height="'unset'">
|
<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">
|
<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="flex items-center justify-between">
|
||||||
|
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
<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">
|
<div class="flex flex-col sm:flex-row text-sm">
|
||||||
<p class="w-32 min-w-32 text-black-50 mb-1">
|
<div class="w-full sm:w-1/2">
|
||||||
{{ key.replace('tag', '') }}
|
<div class="flex mb-1">
|
||||||
</p>
|
<p class="w-32 text-black-50">
|
||||||
<p>{{ value }}</p>
|
{{ $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>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -91,10 +106,24 @@ export default {
|
|||||||
audioFile: {
|
audioFile: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
libraryItemId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
probingFile: false,
|
||||||
|
ffprobeData: null,
|
||||||
|
copiedToClipboard: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.ffprobeData = null
|
||||||
|
this.copiedToClipboard = false
|
||||||
|
this.probingFile = false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
@@ -110,9 +139,36 @@ export default {
|
|||||||
},
|
},
|
||||||
metaTags() {
|
metaTags() {
|
||||||
return this.audioFile?.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() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedOption(action) {
|
clickedOption(action) {
|
||||||
this.$emit('action', action)
|
this.$emit('action', { action })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<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>
|
</div>
|
||||||
</template>
|
</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">
|
<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>
|
<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 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="w-40 px-1 text-gray-200">{{ $strings.LabelLibrary }} Id</div>
|
||||||
<div class="px-1">
|
<div class="px-1 text-xs">
|
||||||
{{ _session.libraryId }}
|
{{ _session.libraryId }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelLibraryItem }} Id</div>
|
<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 }}
|
{{ _session.libraryItemId }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="_session.episodeId" class="flex items-center -mx-1 mb-1">
|
<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="w-40 px-1 text-gray-200">{{ $strings.LabelEpisode }} Id</div>
|
||||||
<div class="px-1">
|
<div class="px-1 text-xs">
|
||||||
{{ _session.episodeId }}
|
{{ _session.episodeId }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full md:w-1/3">
|
<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="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="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
|
||||||
<p class="mb-1">{{ playMethodName }}</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 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-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>
|
<span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||||
</div>
|
</button>
|
||||||
<slot name="outer" />
|
<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 />
|
<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">
|
<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 />
|
<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">
|
<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">
|
<template v-if="!showImageUploader">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div class="flex">
|
<div class="flex flex-wrap">
|
||||||
<div>
|
<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" />
|
<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>
|
||||||
<div class="flex-grow px-4">
|
<div class="flex-grow px-4">
|
||||||
<ui-text-input-with-label v-model="newCollectionName" :label="$strings.LabelName" class="mb-2" />
|
<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>
|
<ui-btn color="success">Upload</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- <modals-upload-image-modal v-model="showUploadImageModal" entity="collection" :entity-id="collection.id" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
@@ -154,7 +151,6 @@ export default {
|
|||||||
availableTabs() {
|
availableTabs() {
|
||||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||||
return this.tabs.filter((tab) => {
|
return this.tabs.filter((tab) => {
|
||||||
if (tab.experimental && !this.showExperimentalFeatures) return false
|
|
||||||
if (tab.mediaType && this.mediaType !== tab.mediaType) return false
|
if (tab.mediaType && this.mediaType !== tab.mediaType) return false
|
||||||
if (tab.admin && !this.userIsAdminOrUp) return false
|
if (tab.admin && !this.userIsAdminOrUp) return false
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<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">
|
<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 -->
|
<!-- 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">
|
<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']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem?.id || null
|
||||||
|
},
|
||||||
|
libraryItemUpdatedAt() {
|
||||||
|
return this.libraryItem?.updatedAt || null
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
return this.libraryItem?.mediaType || null
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType == 'podcast'
|
return this.mediaType == 'podcast'
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
coverPath() {
|
coverPath() {
|
||||||
return this.media.coverPath
|
return this.media.coverPath
|
||||||
@@ -157,7 +160,7 @@ export default {
|
|||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
libraryFiles() {
|
libraryFiles() {
|
||||||
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
return this.libraryItem?.libraryFiles || []
|
||||||
},
|
},
|
||||||
userCanUpload() {
|
userCanUpload() {
|
||||||
return this.$store.getters['user/getUserCanUpload']
|
return this.$store.getters['user/getUserCanUpload']
|
||||||
@@ -280,9 +283,8 @@ export default {
|
|||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
this.$toast.success('Update Successful')
|
this.$toast.success('Update Successful')
|
||||||
// this.$emit('close')
|
} else if (this.media.coverPath) {
|
||||||
} else {
|
this.imageUrl = this.media.coverPath
|
||||||
this.imageUrl = this.media.coverPath || ''
|
|
||||||
}
|
}
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,18 +20,14 @@
|
|||||||
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
||||||
<table v-else class="text-sm tracksTable">
|
<table v-else class="text-sm tracksTable">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left">Sort #</th>
|
<th class="text-center w-20 min-w-20">{{ $strings.LabelEpisode }}</th>
|
||||||
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
<th class="text-left">{{ $strings.LabelEpisodeTitle }}</th>
|
||||||
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
<th class="text-center w-28">{{ $strings.LabelEpisodeDuration }}</th>
|
||||||
<th class="text-center w-28">{{ $strings.EpisodeDuration }}</th>
|
<th class="text-center w-28">{{ $strings.LabelEpisodeSize }}</th>
|
||||||
<th class="text-center w-28">{{ $strings.EpisodeSize }}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="episode in episodes" :key="episode.id">
|
<tr v-for="episode in episodes" :key="episode.id">
|
||||||
<td class="text-left">
|
<td class="text-center w-20 min-w-20">
|
||||||
<p class="px-4">{{ episode.index }}</p>
|
<p>{{ episode.episode }}</p>
|
||||||
</td>
|
|
||||||
<td class="text-left">
|
|
||||||
<p class="px-4">{{ episode.episode }}</p>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ episode.title }}
|
{{ episode.title }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Split to mp3 -->
|
<!-- 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 class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
|
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
|
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- Embed Metadata -->
|
<!-- Embed Metadata -->
|
||||||
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
||||||
@@ -79,9 +79,6 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id || null
|
return this.libraryItem?.id || null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
<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-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
@@ -11,24 +11,44 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
|
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-else disabled :value="false" />
|
<ui-toggle-switch v-else disabled :value="false" />
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsDisableWatcherForLibrary }}</p>
|
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
||||||
</div>
|
</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">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mediaType == 'book'" class="py-3">
|
<div v-if="isBookLibrary" class="py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,9 +65,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
provider: null,
|
provider: null,
|
||||||
useSquareBookCovers: false,
|
useSquareBookCovers: false,
|
||||||
disableWatcher: false,
|
enableWatcher: false,
|
||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
skipMatchingMediaWithIsbn: false
|
skipMatchingMediaWithIsbn: false,
|
||||||
|
audiobooksOnly: false,
|
||||||
|
hideSingleBookSeries: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -60,6 +82,9 @@ export default {
|
|||||||
mediaType() {
|
mediaType() {
|
||||||
return this.library.mediaType
|
return this.library.mediaType
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.mediaType === 'book'
|
||||||
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
@@ -70,9 +95,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||||
disableWatcher: !!this.disableWatcher,
|
disableWatcher: !this.enableWatcher,
|
||||||
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn
|
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
||||||
|
audiobooksOnly: !!this.audiobooksOnly,
|
||||||
|
hideSingleBookSeries: !!this.hideSingleBookSeries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -81,9 +108,11 @@ export default {
|
|||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.useSquareBookCovers = this.librarySettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
this.useSquareBookCovers = this.librarySettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
||||||
this.disableWatcher = !!this.librarySettings.disableWatcher
|
this.enableWatcher = !this.librarySettings.disableWatcher
|
||||||
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
||||||
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
||||||
|
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
||||||
|
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end pt-4">
|
<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>
|
<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>
|
<p v-else class="text-success text-base px-2 py-4">All episodes are downloaded</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,46 +99,82 @@ export default {
|
|||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
},
|
},
|
||||||
buttonText() {
|
buttonText() {
|
||||||
if (!this.episodesSelected.length) return 'No Episodes Selected'
|
if (!this.episodesSelected.length) return this.$strings.LabelNoEpisodesSelected
|
||||||
return `Download ${this.episodesSelected.length} Episode${this.episodesSelected.length > 1 ? 's' : ''}`
|
if (this.episodesSelected.length === 1) return `${this.$strings.LabelDownload} ${this.$strings.LabelEpisode.toLowerCase()}`
|
||||||
|
return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length])
|
||||||
},
|
},
|
||||||
itemEpisodes() {
|
itemEpisodes() {
|
||||||
if (!this.libraryItem) return []
|
if (!this.libraryItem) return []
|
||||||
return this.libraryItem.media.episodes || []
|
return this.libraryItem.media.episodes || []
|
||||||
},
|
},
|
||||||
itemEpisodeMap() {
|
itemEpisodeMap() {
|
||||||
var map = {}
|
const map = {}
|
||||||
this.itemEpisodes.forEach((item) => {
|
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
|
return map
|
||||||
},
|
},
|
||||||
episodesList() {
|
episodesList() {
|
||||||
return this.episodesCleaned.filter((episode) => {
|
return this.episodesCleaned.filter((episode) => {
|
||||||
if (!this.searchText) return true
|
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: {
|
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() {
|
inputUpdate() {
|
||||||
clearTimeout(this.searchTimeout)
|
clearTimeout(this.searchTimeout)
|
||||||
this.searchTimeout = setTimeout(() => {
|
this.searchTimeout = setTimeout(() => {
|
||||||
if (!this.search || !this.search.trim()) {
|
if (!this.search?.trim()) {
|
||||||
this.searchText = ''
|
this.searchText = ''
|
||||||
|
this.checkSetIsSelectedAll()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.searchText = this.search.toLowerCase().trim()
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
|
this.checkSetIsSelectedAll()
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
toggleSelectAll(val) {
|
toggleSelectAll(val) {
|
||||||
for (const episode of this.episodesCleaned) {
|
for (const episode of this.episodesList) {
|
||||||
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
|
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
|
||||||
else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
|
else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkSetIsSelectedAll() {
|
checkSetIsSelectedAll() {
|
||||||
for (const episode of this.episodesCleaned) {
|
for (const episode of this.episodesList) {
|
||||||
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
|
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
return
|
return
|
||||||
@@ -147,19 +183,19 @@ export default {
|
|||||||
this.selectAll = true
|
this.selectAll = true
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(episode) {
|
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.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
|
||||||
this.checkSetIsSelectedAll()
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
var episodesToDownload = []
|
let episodesToDownload = []
|
||||||
if (this.episodesSelected.length) {
|
if (this.episodesSelected.length) {
|
||||||
episodesToDownload = this.episodesSelected.map((cleanUrl) => this.episodesCleaned.find((ep) => ep.cleanUrl == cleanUrl))
|
episodesToDownload = this.episodesSelected.map((cleanUrl) => this.episodesCleaned.find((ep) => ep.cleanUrl == cleanUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
var payloadSize = JSON.stringify(episodesToDownload).length
|
const payloadSize = JSON.stringify(episodesToDownload).length
|
||||||
var sizeInMb = payloadSize / 1024 / 1024
|
const sizeInMb = payloadSize / 1024 / 1024
|
||||||
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||||
console.log('Request size', sizeInMb)
|
console.log('Request size', sizeInMb)
|
||||||
if (sizeInMb > 4.99) {
|
if (sizeInMb > 4.99) {
|
||||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
||||||
@@ -174,10 +210,9 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to download episodes'
|
|
||||||
console.error('Failed to download episodes', error)
|
console.error('Failed to download episodes', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(error.response?.data || 'Failed to download episodes')
|
||||||
|
|
||||||
this.selectedEpisodes = {}
|
this.selectedEpisodes = {}
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
@@ -189,7 +224,7 @@ export default {
|
|||||||
.map((_ep) => {
|
.map((_ep) => {
|
||||||
return {
|
return {
|
||||||
..._ep,
|
..._ep,
|
||||||
cleanUrl: _ep.enclosure.url.split('?')[0]
|
cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
serverAddress: window.origin,
|
serverAddress: window.origin,
|
||||||
slug: this.newFeedSlug,
|
slug: this.newFeedSlug,
|
||||||
@@ -151,6 +153,9 @@ export default {
|
|||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(errorMsg || 'Failed to open RSS Feed')
|
this.$toast.error(errorMsg || 'Failed to open RSS Feed')
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
copyToClipboard(str) {
|
copyToClipboard(str) {
|
||||||
this.$copyToClipboard(str, this)
|
this.$copyToClipboard(str, this)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="rss-feed-view-modal" :processing="processing" :width="700" :height="'unset'">
|
||||||
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
<div v-if="feed" class="w-full">
|
||||||
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
|
||||||
|
<div class="w-full relative">
|
||||||
|
<ui-text-input v-model="feed.feedUrl" readonly />
|
||||||
|
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="feed.meta" class="mt-5">
|
||||||
|
<div class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ feed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="feed.meta.ownerName" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ feed.meta.ownerName }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="feed.meta.ownerEmail" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ feed.meta.ownerEmail }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- -->
|
||||||
|
<div class="episodesTable mt-2">
|
||||||
|
<div class="bg-primary bg-opacity-40 h-12 header">
|
||||||
|
{{ $strings.LabelEpisodeTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="scroller">
|
||||||
|
<div v-for="episode in feed.episodes" :key="episode.id" class="h-8 text-xs truncate">
|
||||||
|
{{ episode.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
feed: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_feed() {
|
||||||
|
return this.feed || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyToClipboard(str) {
|
||||||
|
this.$copyToClipboard(str, this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.episodesTable {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episodesTable div.header {
|
||||||
|
background-color: #272727;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episodesTable .scroller {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episodesTable .scroller div {
|
||||||
|
background-color: #373838;
|
||||||
|
padding: 4px 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 32px;
|
||||||
|
flex: 0 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episodesTable .scroller div:nth-child(even) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<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-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 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-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>
|
<p class="text-sm truncate">{{ file }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,23 +14,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<span class="material-icons text-xl">more</span>
|
||||||
</div>
|
</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>
|
<span class="material-icons text-xl">menu</span>
|
||||||
</div>
|
</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">
|
<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 + 1 }} / {{ numPages }}</p>
|
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden w-full h-full relative">
|
<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">
|
<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>
|
<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>
|
</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">
|
<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>
|
<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>
|
</div>
|
||||||
@@ -61,7 +64,9 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
playerOpen: Boolean
|
playerOpen: Boolean,
|
||||||
|
keepProgress: Boolean,
|
||||||
|
fileId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -71,6 +76,7 @@ export default {
|
|||||||
mainImg: null,
|
mainImg: null,
|
||||||
page: 0,
|
page: 0,
|
||||||
numPages: 0,
|
numPages: 0,
|
||||||
|
pageMenuWidth: 256,
|
||||||
showPageMenu: false,
|
showPageMenu: false,
|
||||||
showInfoMenu: false,
|
showInfoMenu: false,
|
||||||
loadTimeout: null,
|
loadTimeout: null,
|
||||||
@@ -94,19 +100,72 @@ export default {
|
|||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
|
if (this.fileId) {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||||
|
}
|
||||||
return `/api/items/${this.libraryItemId}/ebook`
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
},
|
},
|
||||||
comicMetadataKeys() {
|
comicMetadataKeys() {
|
||||||
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
||||||
},
|
},
|
||||||
canGoNext() {
|
canGoNext() {
|
||||||
return this.page < this.numPages - 1
|
return this.page < this.numPages
|
||||||
},
|
},
|
||||||
canGoPrev() {
|
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: {
|
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() {
|
clickOutside() {
|
||||||
if (this.showPageMenu) this.showPageMenu = false
|
if (this.showPageMenu) this.showPageMenu = false
|
||||||
if (this.showInfoMenu) this.showInfoMenu = false
|
if (this.showInfoMenu) this.showInfoMenu = false
|
||||||
@@ -119,12 +178,15 @@ export default {
|
|||||||
if (!this.canGoPrev) return
|
if (!this.canGoPrev) return
|
||||||
this.setPage(this.page - 1)
|
this.setPage(this.page - 1)
|
||||||
},
|
},
|
||||||
setPage(index) {
|
setPage(page) {
|
||||||
if (index < 0 || index > this.numPages - 1) {
|
if (page <= 0 || page > this.numPages) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var filename = this.pages[index]
|
this.showPageMenu = false
|
||||||
this.page = index
|
this.showInfoMenu = false
|
||||||
|
const filename = this.pages[page - 1]
|
||||||
|
this.page = page
|
||||||
|
this.updateProgress()
|
||||||
return this.extractFile(filename)
|
return this.extractFile(filename)
|
||||||
},
|
},
|
||||||
setLoadTimeout() {
|
setLoadTimeout() {
|
||||||
@@ -174,9 +236,28 @@ export default {
|
|||||||
|
|
||||||
this.numPages = this.pages.length
|
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) {
|
if (this.pages.length) {
|
||||||
this.loading = false
|
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
|
this.loadedFirstPage = true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Unable to extract pages')
|
this.$toast.error('Unable to extract pages')
|
||||||
@@ -222,8 +303,8 @@ export default {
|
|||||||
},
|
},
|
||||||
parseImageFilename(filename) {
|
parseImageFilename(filename) {
|
||||||
var basename = Path.basename(filename, Path.extname(filename))
|
var basename = Path.basename(filename, Path.extname(filename))
|
||||||
var numbersinpath = basename.match(/\d{1,5}/g)
|
var numbersinpath = basename.match(/\d+/g)
|
||||||
if (!numbersinpath || !numbersinpath.length) {
|
if (!numbersinpath?.length) {
|
||||||
return {
|
return {
|
||||||
index: -1,
|
index: -1,
|
||||||
filename
|
filename
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="epub-reader" class="h-full w-full">
|
<div id="epub-reader" class="h-full w-full">
|
||||||
<div class="h-full flex items-center justify-center">
|
<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">
|
<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-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
</div>
|
</button>
|
||||||
<div id="frame" class="w-full" style="height: 80%">
|
<div id="frame" class="w-full" style="height: 80%">
|
||||||
<div id="viewer"></div>
|
<div id="viewer"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center justify-center overflow-x-hidden">
|
<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-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
<span v-if="hasNext" class="material-icons text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,7 +28,9 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
playerOpen: Boolean
|
playerOpen: Boolean,
|
||||||
|
keepProgress: Boolean,
|
||||||
|
fileId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -37,7 +39,13 @@ export default {
|
|||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
book: null,
|
book: null,
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
rendition: null
|
rendition: null,
|
||||||
|
ereaderSettings: {
|
||||||
|
theme: 'dark',
|
||||||
|
fontScale: 100,
|
||||||
|
lineSpacing: 115,
|
||||||
|
spread: 'auto'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -61,12 +69,19 @@ export default {
|
|||||||
},
|
},
|
||||||
/** @returns {Array<ePub.NavItem>} */
|
/** @returns {Array<ePub.NavItem>} */
|
||||||
chapters() {
|
chapters() {
|
||||||
return this.book ? this.book.navigation.toc : []
|
return this.book?.navigation?.toc || []
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
if (!this.libraryItemId) return
|
if (!this.libraryItemId) return
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
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() {
|
localStorageLocationsKey() {
|
||||||
return `ebookLocations-${this.libraryItemId}`
|
return `ebookLocations-${this.libraryItemId}`
|
||||||
},
|
},
|
||||||
@@ -78,18 +93,55 @@ export default {
|
|||||||
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
||||||
return this.windowHeight - 164
|
return this.windowHeight - 164
|
||||||
},
|
},
|
||||||
epubUrl() {
|
ebookUrl() {
|
||||||
|
if (this.fileId) {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||||
|
}
|
||||||
return `/api/items/${this.libraryItemId}/ebook`
|
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: {
|
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() {
|
prev() {
|
||||||
|
if (!this.rendition?.manager) return
|
||||||
return this.rendition?.prev()
|
return this.rendition?.prev()
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
|
if (!this.rendition?.manager) return
|
||||||
return this.rendition?.next()
|
return this.rendition?.next()
|
||||||
},
|
},
|
||||||
goToChapter(href) {
|
goToChapter(href) {
|
||||||
|
if (!this.rendition?.manager) return
|
||||||
return this.rendition?.display(href)
|
return this.rendition?.display(href)
|
||||||
},
|
},
|
||||||
keyUp(e) {
|
keyUp(e) {
|
||||||
@@ -106,6 +158,7 @@ export default {
|
|||||||
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
||||||
*/
|
*/
|
||||||
updateProgress(payload) {
|
updateProgress(payload) {
|
||||||
|
if (!this.keepProgress) return
|
||||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||||
console.error('EpubReader.updateProgress failed:', error)
|
console.error('EpubReader.updateProgress failed:', error)
|
||||||
})
|
})
|
||||||
@@ -197,7 +250,7 @@ export default {
|
|||||||
},
|
},
|
||||||
/** @param {string} location - CFI of the new location */
|
/** @param {string} location - CFI of the new location */
|
||||||
relocated(location) {
|
relocated(location) {
|
||||||
if (this.userMediaProgress?.ebookLocation === location.start.cfi) {
|
if (this.savedEbookLocation === location.start.cfi) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +270,7 @@ export default {
|
|||||||
const reader = this
|
const reader = this
|
||||||
|
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.epubUrl, {
|
reader.book = new ePub(reader.ebookUrl, {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: this.readerHeight - 50,
|
height: this.readerHeight - 50,
|
||||||
openAs: 'epub',
|
openAs: 'epub',
|
||||||
@@ -229,35 +282,30 @@ export default {
|
|||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: this.readerHeight * 0.8
|
height: this.readerHeight * 0.8,
|
||||||
|
spread: 'auto',
|
||||||
|
snap: true,
|
||||||
|
manager: 'continuous',
|
||||||
|
flow: 'paginated'
|
||||||
})
|
})
|
||||||
|
|
||||||
// load saved progress
|
// 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.on('rendered', () => {
|
||||||
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' } })
|
this.applyTheme()
|
||||||
|
})
|
||||||
|
|
||||||
reader.book.ready.then(() => {
|
reader.book.ready.then(() => {
|
||||||
// set up event listeners
|
// set up event listeners
|
||||||
reader.rendition.on('relocated', reader.relocated)
|
reader.rendition.on('relocated', reader.relocated)
|
||||||
reader.rendition.on('keydown', reader.keyUp)
|
reader.rendition.on('keydown', reader.keyUp)
|
||||||
|
|
||||||
let touchStart = 0
|
|
||||||
let touchEnd = 0
|
|
||||||
reader.rendition.on('touchstart', (event) => {
|
reader.rendition.on('touchstart', (event) => {
|
||||||
touchStart = event.changedTouches[0].screenX
|
this.$emit('touchstart', event)
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.rendition.on('touchend', (event) => {
|
reader.rendition.on('touchend', (event) => {
|
||||||
touchEnd = event.changedTouches[0].screenX
|
this.$emit('touchend', event)
|
||||||
const touchDistanceX = Math.abs(touchEnd - touchStart)
|
|
||||||
if (touchStart < touchEnd && touchDistanceX > 120) {
|
|
||||||
this.next()
|
|
||||||
}
|
|
||||||
if (touchStart > touchEnd && touchDistanceX > 120) {
|
|
||||||
this.prev()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// load ebook cfi locations
|
// load ebook cfi locations
|
||||||
@@ -275,6 +323,12 @@ export default {
|
|||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
||||||
|
},
|
||||||
|
applyTheme() {
|
||||||
|
if (!this.rendition) return
|
||||||
|
this.rendition.getContents().forEach((c) => {
|
||||||
|
c.addStylesheetRules(this.themeRules)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
playerOpen: Boolean
|
playerOpen: Boolean,
|
||||||
|
fileId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -32,6 +33,9 @@ export default {
|
|||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
|
if (this.fileId) {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||||
|
}
|
||||||
return `/api/items/${this.libraryItemId}/ebook`
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||||
</div>
|
</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_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" />
|
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +45,9 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
playerOpen: Boolean
|
playerOpen: Boolean,
|
||||||
|
keepProgress: Boolean,
|
||||||
|
fileId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -95,11 +97,21 @@ export default {
|
|||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
savedPage() {
|
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() {
|
pdfDocInitParams() {
|
||||||
return {
|
return {
|
||||||
url: `/api/items/${this.libraryItemId}/ebook`,
|
url: this.ebookUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
Authorization: `Bearer ${this.userToken}`
|
Authorization: `Bearer ${this.userToken}`
|
||||||
}
|
}
|
||||||
@@ -114,6 +126,7 @@ export default {
|
|||||||
this.scale -= 0.1
|
this.scale -= 0.1
|
||||||
},
|
},
|
||||||
updateProgress() {
|
updateProgress() {
|
||||||
|
if (!this.keepProgress) return
|
||||||
if (!this.numPages) {
|
if (!this.numPages) {
|
||||||
console.error('Num pages not loaded')
|
console.error('Num pages not loaded')
|
||||||
return
|
return
|
||||||
@@ -128,7 +141,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadedEvt() {
|
loadedEvt() {
|
||||||
if (this.savedPage && this.savedPage > 0 && this.savedPage <= this.numPages) {
|
if (this.savedPage > 0 && this.savedPage <= this.numPages) {
|
||||||
this.page = this.savedPage
|
this.page = this.savedPage
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,36 +1,48 @@
|
|||||||
<template>
|
<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 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">
|
<div class="absolute top-4 left-4 z-20 flex items-center">
|
||||||
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
<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>
|
||||||
|
|
||||||
<div class="absolute top-4 left-1/2 transform -translate-x-1/2">
|
<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 style="font-weight: 600">{{ abTitle }}</span>
|
||||||
<span v-if="abAuthor" style="display: inline"> – </span>
|
<span v-if="abAuthor" class="hidden md:inline"> – </span>
|
||||||
<span v-if="abAuthor">{{ abAuthor }}</span>
|
<span v-if="abAuthor" class="hidden md:inline">{{ abAuthor }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 right-4 z-20">
|
||||||
<span v-if="hasSettings" class="material-icons cursor-pointer text-2xl" @click="openSettings">settings</span>
|
<button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
<span class="material-icons text-2xl">close</span>
|
||||||
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- 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="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">
|
<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">
|
<div class="tocContent">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
<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">
|
<ul v-if="chapter.subitems.length">
|
||||||
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -38,6 +50,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,8 +92,21 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
touchstartX: 0,
|
||||||
|
touchstartY: 0,
|
||||||
|
touchendX: 0,
|
||||||
|
touchendY: 0,
|
||||||
|
touchstartTime: 0,
|
||||||
|
touchIdentifier: null,
|
||||||
chapters: [],
|
chapters: [],
|
||||||
tocOpen: false
|
tocOpen: false,
|
||||||
|
showSettings: false,
|
||||||
|
ereaderSettings: {
|
||||||
|
theme: 'dark',
|
||||||
|
fontScale: 100,
|
||||||
|
lineSpacing: 115,
|
||||||
|
spread: 'auto'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -65,6 +125,34 @@ export default {
|
|||||||
this.$store.commit('setShowEReader', val)
|
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() {
|
componentName() {
|
||||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
@@ -75,11 +163,8 @@ export default {
|
|||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
hasToC() {
|
|
||||||
return this.isEpub
|
|
||||||
},
|
|
||||||
hasSettings() {
|
hasSettings() {
|
||||||
return false
|
return this.isEpub
|
||||||
},
|
},
|
||||||
abTitle() {
|
abTitle() {
|
||||||
return this.mediaMetadata.title
|
return this.mediaMetadata.title
|
||||||
@@ -103,10 +188,18 @@ export default {
|
|||||||
return this.selectedLibraryItem.folderId
|
return this.selectedLibraryItem.folderId
|
||||||
},
|
},
|
||||||
ebookFile() {
|
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
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
ebookFormat() {
|
ebookFormat() {
|
||||||
if (!this.ebookFile) return null
|
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
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
ebookType() {
|
ebookType() {
|
||||||
@@ -130,14 +223,34 @@ export default {
|
|||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
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: {
|
methods: {
|
||||||
|
readerMounted() {
|
||||||
|
if (this.isEpub) {
|
||||||
|
this.loadEreaderSettings()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settingsUpdated() {
|
||||||
|
this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings)
|
||||||
|
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
|
||||||
|
},
|
||||||
toggleToC() {
|
toggleToC() {
|
||||||
this.tocOpen = !this.tocOpen
|
this.tocOpen = !this.tocOpen
|
||||||
this.chapters = this.$refs.readerComponent.chapters
|
this.chapters = this.$refs.readerComponent.chapters
|
||||||
},
|
},
|
||||||
openSettings() {},
|
openSettings() {
|
||||||
|
this.showSettings = true
|
||||||
|
},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
@@ -155,11 +268,72 @@ export default {
|
|||||||
prev() {
|
prev() {
|
||||||
if (this.$refs.readerComponent?.prev) this.$refs.readerComponent.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() {
|
registerListeners() {
|
||||||
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
||||||
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
|
document.body.addEventListener('touchend', this.touchend)
|
||||||
},
|
},
|
||||||
unregisterListeners() {
|
unregisterListeners() {
|
||||||
this.$eventBus.$off('reader-hotkey', this.hotkey)
|
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() {
|
init() {
|
||||||
this.registerListeners()
|
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;`
|
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log('Data', this.data)
|
|
||||||
|
|
||||||
this.monthLabels = []
|
this.monthLabels = []
|
||||||
var lastMonth = null
|
var lastMonth = null
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex px-4">
|
<div v-if="isBookLibrary" class="flex px-4">
|
||||||
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
|
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -58,26 +58,32 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
totalItems() {
|
totalItems() {
|
||||||
return this.libraryStats ? this.libraryStats.totalItems : 0
|
return this.libraryStats?.totalItems || 0
|
||||||
},
|
},
|
||||||
totalAuthors() {
|
totalAuthors() {
|
||||||
return this.libraryStats ? this.libraryStats.totalAuthors : 0
|
return this.libraryStats?.totalAuthors || 0
|
||||||
},
|
},
|
||||||
numAudioTracks() {
|
numAudioTracks() {
|
||||||
return this.libraryStats ? this.libraryStats.numAudioTracks : 0
|
return this.libraryStats?.numAudioTracks || 0
|
||||||
},
|
},
|
||||||
totalDuration() {
|
totalDuration() {
|
||||||
return this.libraryStats ? this.libraryStats.totalDuration : 0
|
return this.libraryStats?.totalDuration || 0
|
||||||
},
|
},
|
||||||
totalHours() {
|
totalHours() {
|
||||||
return Math.round(this.totalDuration / (60 * 60))
|
return Math.round(this.totalDuration / (60 * 60))
|
||||||
},
|
},
|
||||||
totalSizePretty() {
|
totalSizePretty() {
|
||||||
var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0
|
var totalSize = this.libraryStats?.totalSize || 0
|
||||||
return this.$bytesPretty(totalSize, 1)
|
return this.$bytesPretty(totalSize, 1)
|
||||||
},
|
},
|
||||||
totalSizeNum() {
|
totalSizeNum() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="contextMenuItems.length" class="text-center">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteLibraryFile() {
|
deleteLibraryFile() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'This will delete the file from your file system. Are you sure?',
|
message: this.$strings.MessageConfirmDeleteFile,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="w-full flex flex-row items-center justify-center">
|
<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>
|
<ui-btn v-if="backup.serverVersion && backup.key" 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-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||||
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -80,6 +80,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
downloadBackup(backup) {
|
||||||
|
this.$downloadFile(`${process.env.serverUrl}/api/backups/${backup.id}/download?token=${this.userToken}`)
|
||||||
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
this.showConfirmApply = false
|
this.showConfirmApply = false
|
||||||
|
|
||||||
@@ -91,8 +94,9 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.isBackingUp = false
|
this.isBackingUp = false
|
||||||
console.error('Failed', error)
|
console.error('Failed to apply backup', error)
|
||||||
this.$toast.error(this.$strings.ToastBackupRestoreFailed)
|
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteBackupClick(backup) {
|
deleteBackupClick(backup) {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
</div>
|
||||||
</transition>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
isMissing: Boolean,
|
|
||||||
expanded: Boolean, // start expanded
|
expanded: Boolean, // start expanded
|
||||||
inModal: Boolean
|
inModal: Boolean
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="contextMenuItems.length" class="text-center">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -83,7 +83,7 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteLibraryFile() {
|
deleteLibraryFile() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'This will delete the file from your file system. Are you sure?',
|
message: this.$strings.MessageConfirmDeleteFile,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
editItem(playlistItem) {
|
editItem(playlistItem) {
|
||||||
if (playlistItem.episode) {
|
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)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
} else {
|
} else {
|
||||||
const itemIds = this.items.map((i) => i.libraryItemId)
|
const itemIds = this.items.map((i) => i.libraryItemId)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-sm">{{ user.type }}</td>
|
<td class="text-sm">{{ user.type }}</td>
|
||||||
<td class="hidden lg:table-cell">
|
<td class="hidden lg:table-cell">
|
||||||
<div v-if="usersOnline[user.id]">
|
<div v-if="usersOnline[user.id]?.session?.displayTitle">
|
||||||
<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 class="truncate text-xs">Listening: {{ usersOnline[user.id].session.displayTitle || '' }}</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>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
@@ -83,6 +87,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
deleteUserClick(user) {
|
||||||
if (this.isDeletingUser) return
|
if (this.isDeletingUser) return
|
||||||
if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
|
if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
|
||||||
@@ -114,11 +124,12 @@ export default {
|
|||||||
},
|
},
|
||||||
loadUsers() {
|
loadUsers() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users?include=latestSession')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.users = res.users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
|
console.log('Loaded users', this.users)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.book.id}`, updatePayload)
|
.$patch(`/api/me/progress/${this.book.id}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|||||||
@@ -11,10 +11,6 @@
|
|||||||
<ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
|
<ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="libraries.length" class="text-xs mt-4 text-gray-200">
|
|
||||||
*<strong>{{ $strings.ButtonForceReScan }}</strong> {{ $strings.MessageForceReScanDescription }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
|
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
|
||||||
**<strong>{{ $strings.ButtonMatchBooks }}</strong> {{ $strings.MessageMatchBooksDescription }}
|
**<strong>{{ $strings.ButtonMatchBooks }}</strong> {{ $strings.MessageMatchBooksDescription }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -71,11 +71,6 @@ export default {
|
|||||||
text: this.$strings.ButtonScan,
|
text: this.$strings.ButtonScan,
|
||||||
action: 'scan',
|
action: 'scan',
|
||||||
value: 'scan'
|
value: 'scan'
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.ButtonForceReScan,
|
|
||||||
action: 'force-scan',
|
|
||||||
value: 'force-scan'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.isBookLibrary) {
|
if (this.isBookLibrary) {
|
||||||
@@ -137,26 +132,6 @@ export default {
|
|||||||
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
forceScan() {
|
|
||||||
const payload = {
|
|
||||||
message: this.$strings.MessageConfirmForceReScan,
|
|
||||||
callback: (confirmed) => {
|
|
||||||
if (confirmed) {
|
|
||||||
this.$store
|
|
||||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
|
||||||
.then(() => {
|
|
||||||
this.$toast.success(this.$strings.ToastLibraryScanStarted)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to start scan', error)
|
|
||||||
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'yesNo'
|
|
||||||
}
|
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
|
||||||
},
|
|
||||||
deleteClick() {
|
deleteClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: this.$getString('MessageConfirmDeleteLibrary', [this.library.name]),
|
message: this.$getString('MessageConfirmDeleteLibrary', [this.library.name]),
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ export default {
|
|||||||
.$patch(routepath, updatePayload)
|
.$patch(routepath, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full py-6">
|
<div class="w-full py-6">
|
||||||
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
|
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
||||||
<div class="flex 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 hidden md:block">{{ $strings.HeaderEpisodes }}</p>
|
<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" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<template v-if="isSelectionMode">
|
<div class="flex items-center">
|
||||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<template v-if="isSelectionMode">
|
||||||
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
</ui-tooltip>
|
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||||
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
|
</ui-tooltip>
|
||||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
|
||||||
</template>
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
<template v-else>
|
</template>
|
||||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 sm:ml-4" />
|
<template v-else>
|
||||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" />
|
||||||
<div class="flex-grow md:hidden" />
|
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
<div class="flex-grow md:hidden" />
|
||||||
</template>
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
@@ -51,7 +58,6 @@ export default {
|
|||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false,
|
processing: false,
|
||||||
quickMatchingEpisodes: false,
|
|
||||||
search: null,
|
search: null,
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchText: null
|
searchText: null
|
||||||
@@ -71,6 +77,10 @@ export default {
|
|||||||
{
|
{
|
||||||
text: 'Quick match all episodes',
|
text: 'Quick match all episodes',
|
||||||
action: 'quick-match-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() {
|
episodesList() {
|
||||||
return this.episodesSorted.filter((episode) => {
|
return this.episodesSorted.filter((episode) => {
|
||||||
if (!this.searchText) return true
|
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() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedEpisodes.find((episode) => {
|
return !this.selectedEpisodes.some((episode) => {
|
||||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
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() {
|
dateFormat() {
|
||||||
@@ -187,17 +203,34 @@ export default {
|
|||||||
},
|
},
|
||||||
contextMenuAction({ action }) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
if (this.quickMatchingEpisodes) return
|
if (this.processing) return
|
||||||
|
|
||||||
this.quickMatchAllEpisodes()
|
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() {
|
quickMatchAllEpisodes() {
|
||||||
if (!this.mediaMetadata.feedUrl) {
|
if (!this.mediaMetadata.feedUrl) {
|
||||||
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.quickMatchingEpisodes = true
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
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.$toast.error('Failed to match episodes')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.quickMatchingEpisodes = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
type: 'yesNo'
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
@@ -241,17 +274,19 @@ export default {
|
|||||||
this.$store.commit('addItemToQueue', queueItem)
|
this.$store.commit('addItemToQueue', queueItem)
|
||||||
},
|
},
|
||||||
toggleBatchFinished() {
|
toggleBatchFinished() {
|
||||||
|
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
|
||||||
|
},
|
||||||
|
batchUpdateEpisodesFinished(episodes, newIsFinished) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var newIsFinished = !this.selectedIsFinished
|
|
||||||
var updateProgressPayloads = this.selectedEpisodes.map((episode) => {
|
const updateProgressPayloads = episodes.map((episode) => {
|
||||||
return {
|
return {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return this.$axios
|
||||||
this.$axios
|
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.btn::before {
|
.btn::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu">
|
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||||
<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">
|
<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>
|
<span class="material-icons" :class="iconClass">more_vert</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div v-else class="h-full w-full flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<transition name="menu">
|
<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-for="(item, index) in items">
|
||||||
<template v-if="item.subitems">
|
<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>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</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
|
||||||
<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)">
|
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>
|
<p>{{ subitem.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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)">
|
<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>{{ item.text }}</p>
|
<p class="text-left">{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,9 +52,10 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
menuWidth: {
|
menuWidth: {
|
||||||
type: String,
|
type: Number,
|
||||||
default: '192px'
|
default: 192
|
||||||
}
|
},
|
||||||
|
processing: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -52,12 +64,18 @@ export default {
|
|||||||
events: ['mousedown'],
|
events: ['mousedown'],
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
|
submenuWidth: 144,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
mouseoverItemIndex: null,
|
mouseoverItemIndex: null,
|
||||||
isOverSubItemMenu: false
|
isOverSubItemMenu: false,
|
||||||
|
openSubMenuLeft: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submenuLeftPos() {
|
||||||
|
return this.openSubMenuLeft ? -(this.submenuWidth - 1) : this.menuWidth - 0.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
|
||||||
methods: {
|
methods: {
|
||||||
mouseoverSubItemMenu(index) {
|
mouseoverSubItemMenu(index) {
|
||||||
this.isOverSubItemMenu = true
|
this.isOverSubItemMenu = true
|
||||||
@@ -80,6 +98,12 @@ export default {
|
|||||||
clickShowMenu() {
|
clickShowMenu() {
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.showMenu = !this.showMenu
|
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() {
|
clickedOutside() {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm 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">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||||
@@ -8,7 +16,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px 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">
|
<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)">
|
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
@@ -93,4 +101,10 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.librariesDropdownMenu {
|
||||||
|
max-height: calc(100vh - 75px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
value: [String, Number],
|
value: [String, Number],
|
||||||
label: String,
|
label: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
readonly: Boolean,
|
||||||
rows: {
|
rows: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 2
|
default: 2
|
||||||
|
|||||||
@@ -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
|
// Reload HTML content
|
||||||
this.$refs.trix.editor.loadHTML(newContent)
|
this.$refs.trix.editor.loadHTML(newContent)
|
||||||
// Move cursor to end of new content updated
|
// 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() {
|
getContentEndPosition() {
|
||||||
return this.$refs.trix.editor.getDocument().toString().length - 1
|
return this.$refs.trix.editor.getDocument().toString().length - 1
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<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-for="(item, index) in items">
|
||||||
<template v-if="item.subitems">
|
<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>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</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-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 hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.func, subitem.data)">
|
<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>
|
<p>{{ subitem.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,11 +33,18 @@ export default {
|
|||||||
events: ['mousedown'],
|
events: ['mousedown'],
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
|
submenuWidth: 144,
|
||||||
|
menuWidth: 144,
|
||||||
mouseoverItemIndex: null,
|
mouseoverItemIndex: null,
|
||||||
isOverSubItemMenu: false
|
isOverSubItemMenu: false,
|
||||||
|
openSubMenuLeft: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submenuLeftPos() {
|
||||||
|
return this.openSubMenuLeft ? -this.submenuWidth : this.menuWidth - 1.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
|
||||||
methods: {
|
methods: {
|
||||||
mouseoverSubItemMenu(index) {
|
mouseoverSubItemMenu(index) {
|
||||||
this.isOverSubItemMenu = true
|
this.isOverSubItemMenu = true
|
||||||
@@ -77,7 +84,14 @@ export default {
|
|||||||
this.$el.parentNode.removeChild(this.$el)
|
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() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -343,6 +343,10 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('libraries/removeCollection', collection)
|
this.$store.commit('libraries/removeCollection', collection)
|
||||||
},
|
},
|
||||||
|
seriesRemoved({ id, libraryId }) {
|
||||||
|
if (this.currentLibraryId !== libraryId) return
|
||||||
|
this.$store.commit('libraries/removeSeriesFromFilterData', id)
|
||||||
|
},
|
||||||
playlistAdded(playlist) {
|
playlistAdded(playlist) {
|
||||||
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
@@ -442,6 +446,9 @@ export default {
|
|||||||
this.socket.on('collection_updated', this.collectionUpdated)
|
this.socket.on('collection_updated', this.collectionUpdated)
|
||||||
this.socket.on('collection_removed', this.collectionRemoved)
|
this.socket.on('collection_removed', this.collectionRemoved)
|
||||||
|
|
||||||
|
// Series Listeners
|
||||||
|
this.socket.on('series_removed', this.seriesRemoved)
|
||||||
|
|
||||||
// User Playlist Listeners
|
// User Playlist Listeners
|
||||||
this.socket.on('playlist_added', this.playlistAdded)
|
this.socket.on('playlist_added', this.playlistAdded)
|
||||||
this.socket.on('playlist_updated', this.playlistUpdated)
|
this.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
@@ -491,9 +498,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkActiveElementIsInput() {
|
checkActiveElementIsInput() {
|
||||||
var activeElement = document.activeElement
|
const activeElement = document.activeElement
|
||||||
var inputs = ['input', 'select', 'button', 'textarea']
|
const inputs = ['input', 'select', 'button', 'textarea', 'trix-editor']
|
||||||
return activeElement && inputs.indexOf(activeElement.tagName.toLowerCase()) !== -1
|
return activeElement && inputs.some((i) => i === activeElement.tagName.toLowerCase())
|
||||||
},
|
},
|
||||||
getHotkeyName(e) {
|
getHotkeyName(e) {
|
||||||
var keyCode = e.keyCode || e.which
|
var keyCode = e.keyCode || e.which
|
||||||
@@ -560,12 +567,6 @@ export default {
|
|||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
},
|
},
|
||||||
initLocalStorage() {
|
initLocalStorage() {
|
||||||
// If experimental features set in local storage
|
|
||||||
var experimentalFeaturesSaved = localStorage.getItem('experimental')
|
|
||||||
if (experimentalFeaturesSaved === '1') {
|
|
||||||
this.$store.commit('setExperimentalFeatures', true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue auto play
|
// Queue auto play
|
||||||
var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
|
var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
|
||||||
this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
|
this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
proxy: {
|
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: {
|
io: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.22",
|
"version": "2.4.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.22",
|
"version": "2.4.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.22",
|
"version": "2.4.1",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -112,17 +112,17 @@
|
|||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
||||||
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
||||||
<div class="w-20 text-center">{{ $strings.HeaderChapters }}</div>
|
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="track in audioTracks">
|
<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 :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>
|
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-20" style="min-width: 80px">
|
<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>
|
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
|
||||||
</div>
|
</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>
|
<span v-if="(track.chapters || []).length" class="material-icons text-success text-sm">check</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||||
<div class="flex flex-wrap -mx-2">
|
<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="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" />
|
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" />
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +214,7 @@ export default {
|
|||||||
showEncodeOptions: false,
|
showEncodeOptions: false,
|
||||||
shouldBackupAudioFiles: true,
|
shouldBackupAudioFiles: true,
|
||||||
encodingOptions: {
|
encodingOptions: {
|
||||||
bitrate: '64k',
|
bitrate: '128k',
|
||||||
channels: '2',
|
channels: '2',
|
||||||
codec: 'aac'
|
codec: 'aac'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
|
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
||||||
else if (pageName === 'email') return this.$strings.HeaderEmail
|
else if (pageName === 'email') return this.$strings.HeaderEmail
|
||||||
}
|
}
|
||||||
return this.$strings.HeaderSettings
|
return this.$strings.HeaderSettings
|
||||||
|
|||||||
@@ -11,14 +11,18 @@
|
|||||||
<div v-if="enableBackups" class="mb-6">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<div class="flex items-center pl-6 mb-2">
|
<div class="flex items-center pl-6 mb-2">
|
||||||
<span class="material-icons-outlined text-2xl text-black-50 mr-2">schedule</span>
|
<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>
|
<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>
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer ml-2" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="nextBackupDate" class="flex items-center pl-6 py-0.5 px-2">
|
<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>
|
<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 class="text-gray-100">{{ nextBackupDate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,6 +52,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
@@ -98,7 +107,7 @@ export default {
|
|||||||
this.$toast.error('Invalid number of backups to keep')
|
this.$toast.error('Invalid number of backups to keep')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var updatePayload = {
|
const updatePayload = {
|
||||||
backupSchedule: this.enableBackups ? this.cronExpression : false,
|
backupSchedule: this.enableBackups ? this.cronExpression : false,
|
||||||
backupsToKeep: Number(this.backupsToKeep),
|
backupsToKeep: Number(this.backupsToKeep),
|
||||||
maxBackupSize: Number(this.maxBackupSize)
|
maxBackupSize: Number(this.maxBackupSize)
|
||||||
@@ -108,15 +117,15 @@ export default {
|
|||||||
updateServerSettings(payload) {
|
updateServerSettings(payload) {
|
||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('updateServerSettings', payload)
|
.dispatch('updateServerSettings', payload)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
console.log('Updated Server Settings', success)
|
console.log('Updated Server Settings', success)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
|
|||||||
@@ -34,10 +34,14 @@
|
|||||||
<div class="w-full md:w-1/2 px-1">
|
<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" />
|
<ui-text-input-with-label ref="fromInput" v-model="newSettings.fromAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsFromAddress" />
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="flex items-center justify-between pt-4">
|
<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>
|
<ui-btn :loading="savingSettings" :disabled="!hasUpdates" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -47,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</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">
|
<table v-if="existingEReaderDevices.length" class="tracksTable my-4">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left">{{ $strings.LabelName }}</th>
|
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||||
@@ -80,6 +84,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -93,6 +102,7 @@ export default {
|
|||||||
secure: true,
|
secure: true,
|
||||||
user: null,
|
user: null,
|
||||||
pass: null,
|
pass: null,
|
||||||
|
testAddress: null,
|
||||||
fromAddress: null
|
fromAddress: null
|
||||||
},
|
},
|
||||||
newEReaderDevice: {
|
newEReaderDevice: {
|
||||||
@@ -117,6 +127,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
resetChanges() {
|
||||||
|
this.newSettings = {
|
||||||
|
...this.settings
|
||||||
|
}
|
||||||
|
},
|
||||||
editDeviceClick(device) {
|
editDeviceClick(device) {
|
||||||
this.selectedEReaderDevice = device
|
this.selectedEReaderDevice = device
|
||||||
this.showEReaderDeviceModal = true
|
this.showEReaderDeviceModal = true
|
||||||
@@ -139,7 +154,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.deletingDeviceName = device.name
|
this.deletingDeviceName = device.name
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/emails/ereader-devices`, payload)
|
.$post(`/api/emails/ereader-devices`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||||
this.$toast.success('Device deleted')
|
this.$toast.success('Device deleted')
|
||||||
@@ -196,6 +211,7 @@ export default {
|
|||||||
secure: this.newSettings.secure,
|
secure: this.newSettings.secure,
|
||||||
user: this.newSettings.user,
|
user: this.newSettings.user,
|
||||||
pass: this.newSettings.pass,
|
pass: this.newSettings.pass,
|
||||||
|
testAddress: this.newSettings.testAddress,
|
||||||
fromAddress: this.newSettings.fromAddress
|
fromAddress: this.newSettings.fromAddress
|
||||||
}
|
}
|
||||||
this.savingSettings = true
|
this.savingSettings = true
|
||||||
|
|||||||
@@ -36,7 +36,10 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
||||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="sortingPrefixesUpdated" :disabled="savingPrefixes" />
|
||||||
|
<div class="flex justify-end py-1">
|
||||||
|
<ui-btn v-if="hasPrefixesChanged" color="success" :loading="savingPrefixes" small @click="updateSortingPrefixes">Save</ui-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2 mb-2">
|
<div class="flex items-center py-2 mb-2">
|
||||||
@@ -157,16 +160,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
<span id="settings-disable-watcher">{{ $strings.LabelSettingsDisableWatcher }}</span>
|
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<!-- old experimental features -->
|
||||||
|
<!-- <div class="pt-4">
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,26 +184,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +195,6 @@
|
|||||||
<div class="flex-grow" />
|
<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="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="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>
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
@@ -268,15 +251,23 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isResettingLibraryItems: false,
|
isResettingLibraryItems: false,
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
homepageUseBookshelfView: false,
|
homepageUseBookshelfView: false,
|
||||||
useBookshelfView: false,
|
useBookshelfView: false,
|
||||||
|
scannerEnableWatcher: false,
|
||||||
isPurgingCache: false,
|
isPurgingCache: false,
|
||||||
|
hasPrefixesChanged: false,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
showConfirmPurgeCache: false,
|
showConfirmPurgeCache: false,
|
||||||
|
savingPrefixes: false,
|
||||||
metadataFileFormats: [
|
metadataFileFormats: [
|
||||||
{
|
{
|
||||||
text: '.json',
|
text: '.json',
|
||||||
@@ -303,14 +294,6 @@ export default {
|
|||||||
providers() {
|
providers() {
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
},
|
},
|
||||||
showExperimentalFeatures: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('setExperimentalFeatures', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dateFormats() {
|
dateFormats() {
|
||||||
return this.$store.state.globals.dateFormats
|
return this.$store.state.globals.dateFormats
|
||||||
},
|
},
|
||||||
@@ -327,15 +310,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateSortingPrefixes(val) {
|
sortingPrefixesUpdated(val) {
|
||||||
if (!val || !val.length) {
|
const prefixes = [...new Set(val?.map((prefix) => prefix.trim().toLowerCase()) || [])]
|
||||||
|
this.newServerSettings.sortingPrefixes = prefixes
|
||||||
|
const serverPrefixes = this.serverSettings.sortingPrefixes || []
|
||||||
|
this.hasPrefixesChanged = prefixes.some((p) => !serverPrefixes.includes(p)) || serverPrefixes.some((p) => !prefixes.includes(p))
|
||||||
|
},
|
||||||
|
updateSortingPrefixes() {
|
||||||
|
const prefixes = [...new Set(this.newServerSettings.sortingPrefixes.map((prefix) => prefix.trim().toLowerCase()) || [])]
|
||||||
|
if (!prefixes.length) {
|
||||||
this.$toast.error('Must have at least 1 prefix')
|
this.$toast.error('Must have at least 1 prefix')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var prefixes = val.map((prefix) => prefix.trim().toLowerCase())
|
|
||||||
this.updateServerSettings({
|
this.savingPrefixes = true
|
||||||
sortingPrefixes: prefixes
|
this.$axios
|
||||||
})
|
.$patch(`/api/sorting-prefixes`, { sortingPrefixes: prefixes })
|
||||||
|
.then((data) => {
|
||||||
|
this.$toast.success(`Sorting prefixes updated. ${data.rowsUpdated} rows`)
|
||||||
|
if (data.serverSettings) {
|
||||||
|
this.$store.commit('setServerSettings', data.serverSettings)
|
||||||
|
}
|
||||||
|
this.hasPrefixesChanged = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update prefixes', error)
|
||||||
|
this.$toast.error('Failed to update sorting prefixes')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.savingPrefixes = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateScannerCoverProvider(val) {
|
updateScannerCoverProvider(val) {
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
@@ -360,6 +364,9 @@ export default {
|
|||||||
this.updateSettingsKey('metadataFileFormat', val)
|
this.updateSettingsKey('metadataFileFormat', val)
|
||||||
},
|
},
|
||||||
updateSettingsKey(key, val) {
|
updateSettingsKey(key, val) {
|
||||||
|
if (key === 'scannerDisableWatcher') {
|
||||||
|
this.newServerSettings.scannerDisableWatcher = val
|
||||||
|
}
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
[key]: val
|
[key]: val
|
||||||
})
|
})
|
||||||
@@ -386,27 +393,11 @@ export default {
|
|||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||||
|
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
||||||
|
|
||||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||||
this.useBookshelfView = this.newServerSettings.bookshelfView != 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() {
|
purgeCache() {
|
||||||
this.showConfirmPurgeCache = true
|
this.showConfirmPurgeCache = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showLibraryModal: false,
|
showLibraryModal: false,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||||
<template v-for="(author, index) in top10Authors">
|
<template v-for="(author, index) in top10Authors">
|
||||||
@@ -87,6 +87,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ redirect, store }) {
|
asyncData({ redirect, store }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!store.state.libraries.currentLibraryId) {
|
if (!store.state.libraries.currentLibraryId) {
|
||||||
return redirect('/config')
|
return redirect('/config')
|
||||||
}
|
}
|
||||||
@@ -109,43 +114,49 @@ export default {
|
|||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
totalItems() {
|
totalItems() {
|
||||||
return this.libraryStats ? this.libraryStats.totalItems : 0
|
return this.libraryStats?.totalItems || 0
|
||||||
},
|
},
|
||||||
genresWithCount() {
|
genresWithCount() {
|
||||||
return this.libraryStats ? this.libraryStats.genresWithCount : []
|
return this.libraryStats?.genresWithCount || []
|
||||||
},
|
},
|
||||||
top5Genres() {
|
top5Genres() {
|
||||||
return this.genresWithCount.slice(0, 5)
|
return this.genresWithCount?.slice(0, 5) || []
|
||||||
},
|
},
|
||||||
top10LongestItems() {
|
top10LongestItems() {
|
||||||
return this.libraryStats ? this.libraryStats.longestItems || [] : []
|
return this.libraryStats?.longestItems || []
|
||||||
},
|
},
|
||||||
longestItemDuration() {
|
longestItemDuration() {
|
||||||
if (!this.top10LongestItems.length) return 0
|
if (!this.top10LongestItems.length) return 0
|
||||||
return this.top10LongestItems[0].duration
|
return this.top10LongestItems[0].duration
|
||||||
},
|
},
|
||||||
top10LargestItems() {
|
top10LargestItems() {
|
||||||
return this.libraryStats ? this.libraryStats.largestItems || [] : []
|
return this.libraryStats?.largestItems || []
|
||||||
},
|
},
|
||||||
largestItemSize() {
|
largestItemSize() {
|
||||||
if (!this.top10LargestItems.length) return 0
|
if (!this.top10LargestItems.length) return 0
|
||||||
return this.top10LargestItems[0].size
|
return this.top10LargestItems[0].size
|
||||||
},
|
},
|
||||||
authorsWithCount() {
|
authorsWithCount() {
|
||||||
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
return this.libraryStats?.authorsWithCount || []
|
||||||
},
|
},
|
||||||
mostUsedAuthorCount() {
|
mostUsedAuthorCount() {
|
||||||
if (!this.authorsWithCount.length) return 0
|
if (!this.authorsWithCount.length) return 0
|
||||||
return this.authorsWithCount[0].count
|
return this.authorsWithCount[0].count
|
||||||
},
|
},
|
||||||
top10Authors() {
|
top10Authors() {
|
||||||
return this.authorsWithCount.slice(0, 10)
|
return this.authorsWithCount?.slice(0, 10) || []
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
|
},
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: null,
|
search: null,
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="$strings.HeaderRSSFeeds">
|
||||||
|
<div v-if="feeds.length" class="block max-w-full">
|
||||||
|
<table class="rssFeedsTable text-xs">
|
||||||
|
<tr class="bg-primary bg-opacity-40 h-12">
|
||||||
|
<th class="w-16 min-w-16"></th>
|
||||||
|
<th class="w-48 max-w-64 min-w-24 text-left truncate">{{ $strings.LabelTitle }}</th>
|
||||||
|
<th class="w-48 min-w-24 text-left hidden xl:table-cell">{{ $strings.LabelSlug }}</th>
|
||||||
|
<th class="w-24 min-w-16 text-left hidden md:table-cell">{{ $strings.LabelType }}</th>
|
||||||
|
<th class="w-16 min-w-16 text-center">{{ $strings.HeaderEpisodes }}</th>
|
||||||
|
<th class="w-16 min-w-16 text-center hidden lg:table-cell">{{ $strings.LabelRSSFeedPreventIndexing }}</th>
|
||||||
|
<th class="w-48 min-w-24 flex-grow hidden md:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
||||||
|
<th class="w-16 text-left"></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
|
||||||
|
<!-- -->
|
||||||
|
<td>
|
||||||
|
<img :src="coverUrl(feed)" class="h-full w-full" />
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="w-48 max-w-64 min-w-24 text-left truncate">
|
||||||
|
<p class="truncate">{{ feed.meta.title }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="hidden xl:table-cell">
|
||||||
|
<p class="truncate">{{ feed.slug }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="hidden md:table-cell">
|
||||||
|
<p class="">{{ getEntityType(feed.entityType) }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="text-center">
|
||||||
|
<p class="">{{ feed.episodes.length }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="text-center leading-none hidden lg:table-cell">
|
||||||
|
<p v-if="feed.meta.preventIndexing" class="">
|
||||||
|
<span class="material-icons text-2xl">check</span>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="text-center hidden md:table-cell">
|
||||||
|
<ui-tooltip v-if="feed.updatedAt" direction="top" :text="$formatDatetime(feed.updatedAt, dateFormat, timeFormat)">
|
||||||
|
<p class="text-gray-200">{{ $dateDistanceFromNow(feed.updatedAt) }}</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
<!-- -->
|
||||||
|
<td class="text-center">
|
||||||
|
<ui-icon-btn icon="delete" class="mx-0.5" :size="7" bg-color="error" outlined @click.stop="deleteFeedClick(feed)" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</app-settings-content>
|
||||||
|
<modals-rssfeed-view-feed-modal v-model="showFeedModal" :feed="selectedFeed" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showFeedModal: false,
|
||||||
|
selectedFeed: null,
|
||||||
|
feeds: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showFeed(feed) {
|
||||||
|
this.selectedFeed = feed
|
||||||
|
this.showFeedModal = true
|
||||||
|
},
|
||||||
|
deleteFeedClick(feed) {
|
||||||
|
const payload = {
|
||||||
|
message: this.$strings.MessageConfirmCloseFeed,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.deleteFeed(feed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
deleteFeed(feed) {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/feeds/${feed.id}/close`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
||||||
|
this.show = false
|
||||||
|
this.loadFeeds()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to close RSS feed', error)
|
||||||
|
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getEntityType(entityType) {
|
||||||
|
if (entityType === 'libraryItem') return this.$strings.LabelItem
|
||||||
|
else if (entityType === 'series') return this.$strings.LabelSeries
|
||||||
|
else if (entityType === 'collection') return this.$strings.LabelCollection
|
||||||
|
return this.$strings.LabelUnknown
|
||||||
|
},
|
||||||
|
coverUrl(feed) {
|
||||||
|
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
||||||
|
return `${feed.feedUrl}/cover`
|
||||||
|
},
|
||||||
|
async loadFeeds() {
|
||||||
|
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
||||||
|
console.error('Failed to load RSS feeds', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!data) {
|
||||||
|
this.$toast.error('Failed to load RSS feeds')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.feeds = data.feeds
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loadFeeds()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rssFeedsTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable tr:first-child {
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable tr:not(:first-child) {
|
||||||
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable tr:not(:first-child):nth-child(odd) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable tr:hover:not(:first-child) {
|
||||||
|
background-color: #474747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rssFeedsTable th {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -104,7 +104,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ store, redirect, app }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const users = await app.$axios
|
const users = await app.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<div class="flex mb-4 items-center">
|
<div class="flex mb-4 items-center">
|
||||||
<h1 class="text-2xl">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
<h1 class="text-2xl">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||||
<div class="flex-grow" />
|
<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>
|
</div>
|
||||||
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
|
||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
@@ -82,6 +82,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
@@ -116,7 +119,6 @@ export default {
|
|||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Loaded user listening data', this.listeningStats)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -47,13 +47,7 @@
|
|||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
|
<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">
|
<table v-if="mediaProgress.length" class="userAudiobooksTable">
|
||||||
<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">
|
<tr class="bg-primary bg-opacity-40">
|
||||||
<th class="w-16 text-left">{{ $strings.LabelItem }}</th>
|
<th class="w-16 text-left">{{ $strings.LabelItem }}</th>
|
||||||
<th class="text-left"></th>
|
<th class="text-left"></th>
|
||||||
@@ -61,19 +55,14 @@
|
|||||||
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
|
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
|
||||||
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
<tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||||
<td>
|
<td>
|
||||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover v-if="item.coverPath" :width="50" :src="$store.getters['globals/getLibraryItemCoverSrcById'](item.libraryItemId, item.mediaUpdatedAt)" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
||||||
|
<div v-else class="bg-primary flex items-center justify-center text-center text-xs text-gray-400 p-1" :style="{ width: '50px', height: 50 * bookCoverAspectRatio + 'px' }">No Cover</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="item.media && item.media.metadata && item.episode">
|
<p>{{ item.displayTitle || 'Unknown' }}</p>
|
||||||
<p>{{ item.episode.title || 'Unknown' }}</p>
|
<p v-if="item.displaySubtitle" class="text-white text-opacity-50 text-sm font-sans">{{ item.displaySubtitle }}</p>
|
||||||
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.media && item.media.metadata">
|
|
||||||
<p>{{ item.media.metadata.title || 'Unknown' }}</p>
|
|
||||||
<p v-if="item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
|
|
||||||
</template>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
||||||
@@ -111,8 +100,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listeningSessions: {},
|
listeningSessions: {},
|
||||||
listeningStats: {},
|
listeningStats: {}
|
||||||
purgingMediaProgress: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -131,12 +119,6 @@ export default {
|
|||||||
mediaProgress() {
|
mediaProgress() {
|
||||||
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
},
|
},
|
||||||
mediaProgressWithMedia() {
|
|
||||||
return this.mediaProgress.filter((mp) => mp.media)
|
|
||||||
},
|
|
||||||
mediaProgressWithoutMedia() {
|
|
||||||
return this.mediaProgress.filter((mp) => !mp.media)
|
|
||||||
},
|
|
||||||
totalListeningTime() {
|
totalListeningTime() {
|
||||||
return this.listeningStats.totalTime || 0
|
return this.listeningStats.totalTime || 0
|
||||||
},
|
},
|
||||||
@@ -176,24 +158,6 @@ export default {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Loaded user listening data', this.listeningSessions, this.listeningStats)
|
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() {
|
mounted() {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedAccount: null,
|
selectedAccount: null,
|
||||||
|
|||||||
@@ -52,13 +52,6 @@
|
|||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
</div>
|
</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 -->
|
<!-- 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 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">
|
<div class="flex items-center">
|
||||||
@@ -122,7 +115,7 @@
|
|||||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||||
</ui-tooltip>
|
</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 }">
|
<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">
|
<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>
|
<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-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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +160,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Include episode downloads for podcasts
|
// Include episode downloads for podcasts
|
||||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors,downloads,rssfeed`).catch((error) => {
|
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -200,12 +195,6 @@ export default {
|
|||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.state.serverSettings.dateFormat
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
enableEReader() {
|
|
||||||
return this.$store.getters['getServerSetting']('enableEReader')
|
|
||||||
},
|
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
@@ -257,7 +246,7 @@ export default {
|
|||||||
return this.tracks.length
|
return this.tracks.length
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return this.ebookFile && (this.showExperimentalFeatures || this.enableEReader)
|
return this.ebookFile
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.libraryItem.libraryId
|
return this.libraryItem.libraryId
|
||||||
@@ -320,6 +309,9 @@ export default {
|
|||||||
libraryFiles() {
|
libraryFiles() {
|
||||||
return this.libraryItem.libraryFiles || []
|
return this.libraryItem.libraryFiles || []
|
||||||
},
|
},
|
||||||
|
ebookFiles() {
|
||||||
|
return this.libraryFiles.filter((lf) => lf.fileType === 'ebook')
|
||||||
|
},
|
||||||
ebookFile() {
|
ebookFile() {
|
||||||
return this.media.ebookFile
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
@@ -330,9 +322,6 @@ export default {
|
|||||||
// Music track
|
// Music track
|
||||||
return this.media.audioFile
|
return this.media.audioFile
|
||||||
},
|
},
|
||||||
showExperimentalReadAlert() {
|
|
||||||
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
|
||||||
},
|
|
||||||
description() {
|
description() {
|
||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
@@ -519,7 +508,7 @@ export default {
|
|||||||
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' })
|
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' })
|
||||||
},
|
},
|
||||||
openEbook() {
|
openEbook() {
|
||||||
this.$store.commit('showEReader', this.libraryItem)
|
this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: true })
|
||||||
},
|
},
|
||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
|
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
|
||||||
@@ -544,7 +533,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
@@ -773,6 +761,7 @@ export default {
|
|||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
}
|
}
|
||||||
|
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
@@ -781,6 +770,7 @@ export default {
|
|||||||
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||||
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="flex items-center">
|
<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>
|
<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-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>
|
</div>
|
||||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
<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>
|
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||||
@@ -146,11 +146,15 @@ export default {
|
|||||||
async submitSearch(term) {
|
async submitSearch(term) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.termSearched = ''
|
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)
|
console.error('Search request failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Got results', results)
|
console.log('Got results', results)
|
||||||
|
|
||||||
|
// Filter out podcasts without an RSS feed
|
||||||
|
results = results.filter((r) => r.feedUrl)
|
||||||
|
|
||||||
for (let result of results) {
|
for (let result of results) {
|
||||||
let podcast = this.existentPodcasts.find((p) => p.itunesId === result.id || p.title === result.title.toLowerCase())
|
let podcast = this.existentPodcasts.find((p) => p.itunesId === result.id || p.title === result.title.toLowerCase())
|
||||||
if (podcast) {
|
if (podcast) {
|
||||||
@@ -164,7 +168,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async selectPodcast(podcast) {
|
async selectPodcast(podcast) {
|
||||||
console.log('Selected podcast', podcast)
|
console.log('Selected podcast', podcast)
|
||||||
if(podcast.existentId){
|
if (podcast.existentId) {
|
||||||
this.$router.push(`/item/${podcast.existentId}`)
|
this.$router.push(`/item/${podcast.existentId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,7 +177,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
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)
|
console.error('Failed to get feed', error)
|
||||||
this.$toast.error('Failed to get podcast feed')
|
this.$toast.error('Failed to get podcast feed')
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default {
|
|||||||
return redirect(`/library/${libraryId}`)
|
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)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-6
@@ -74,9 +74,17 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$router.replace('/oops?message=No libraries available')
|
this.$router.replace('/oops?message=No libraries available')
|
||||||
}
|
}
|
||||||
} else if (this.$route.query.redirect) {
|
|
||||||
this.$router.replace(this.$route.query.redirect)
|
|
||||||
} else {
|
} 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}`)
|
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,17 +152,17 @@ export default {
|
|||||||
this.error = null
|
this.error = null
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
var payload = {
|
const payload = {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password || ''
|
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)
|
console.error('Failed', error.response)
|
||||||
if (error.response) this.error = error.response.data
|
if (error.response) this.error = error.response.data
|
||||||
else this.error = 'Unknown Error'
|
else this.error = 'Unknown Error'
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (authRes && authRes.error) {
|
if (authRes?.error) {
|
||||||
this.error = authRes.error
|
this.error = authRes.error
|
||||||
} else if (authRes) {
|
} else if (authRes) {
|
||||||
this.setUser(authRes)
|
this.setUser(authRes)
|
||||||
@@ -162,7 +170,7 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
checkAuth() {
|
checkAuth() {
|
||||||
var token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
|
clientName: 'Abs Web',
|
||||||
deviceId: this.getDeviceId()
|
deviceId: this.getDeviceId()
|
||||||
},
|
},
|
||||||
supportedMimeTypes: this.player.playableMimeTypes,
|
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() {
|
startPlayInterval() {
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
let lastTick = Date.now()
|
let lastTick = Date.now()
|
||||||
@@ -293,7 +298,7 @@ export default class PlayerHandler {
|
|||||||
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
||||||
lastTick = Date.now()
|
lastTick = Date.now()
|
||||||
this.listeningTimeSinceSync += exactTimeElapsed
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 5 : 20
|
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 10 : 20
|
||||||
if (this.listeningTimeSinceSync >= TimeToWaitBeforeSync) {
|
if (this.listeningTimeSinceSync >= TimeToWaitBeforeSync) {
|
||||||
this.sendProgressSync(currentTime)
|
this.sendProgressSync(currentTime)
|
||||||
}
|
}
|
||||||
@@ -315,7 +320,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.lastSyncTime = 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)
|
console.error('Failed to close session', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -335,12 +340,13 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.listeningTimeSinceSync = 0
|
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
|
this.failedProgressSyncs = 0
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Failed to update session progress', error)
|
console.error('Failed to update session progress', error)
|
||||||
|
// After 4 failed sync attempts show an alert toast
|
||||||
this.failedProgressSyncs++
|
this.failedProgressSyncs++
|
||||||
if (this.failedProgressSyncs >= 2) {
|
if (this.failedProgressSyncs >= 4) {
|
||||||
this.ctx.showFailedProgressSyncs()
|
this.ctx.showFailedProgressSyncs()
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export default function ({ $axios, store, $config }) {
|
|||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
config.headers.common['Authorization'] = `Bearer ${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 => {
|
$axios.onError(error => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const SupportedFileTypes = {
|
const SupportedFileTypes = {
|
||||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
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'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const languageCodeMap = {
|
|||||||
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
|
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' },
|
||||||
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
|
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
|
||||||
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
||||||
@@ -34,7 +35,7 @@ Vue.prototype.$strings = { ...enUsStrings }
|
|||||||
|
|
||||||
Vue.prototype.$getString = (key, subs) => {
|
Vue.prototype.$getString = (key, subs) => {
|
||||||
if (!Vue.prototype.$strings[key]) return ''
|
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 supplant(Vue.prototype.$strings[key], subs)
|
||||||
}
|
}
|
||||||
return Vue.prototype.$strings[key]
|
return Vue.prototype.$strings[key]
|
||||||
|
|||||||
@@ -24,20 +24,20 @@ Vue.prototype.$formatJsDate = (jsdate, fnsFormat = 'MM/dd/yyyy HH:mm') => {
|
|||||||
return format(jsdate, fnsFormat)
|
return format(jsdate, fnsFormat)
|
||||||
}
|
}
|
||||||
Vue.prototype.$formatTime = (unixms, fnsFormat = 'HH:mm') => {
|
Vue.prototype.$formatTime = (unixms, fnsFormat = 'HH:mm') => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return format(unixms, fnsFormat)
|
return format(unixms, fnsFormat)
|
||||||
}
|
}
|
||||||
Vue.prototype.$formatJsTime = (jsdate, fnsFormat = 'HH:mm') => {
|
Vue.prototype.$formatJsTime = (jsdate, fnsFormat = 'HH:mm') => {
|
||||||
if (!jsdate || !isDate(jsdate)) return ''
|
if (!jsdate || !isDate(jsdate)) return ''
|
||||||
return format(jsdate, fnsFormat)
|
return format(jsdate, fnsFormat)
|
||||||
}
|
}
|
||||||
Vue.prototype.$formatDatetime = (unixms, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
Vue.prototype.$formatDatetime = (unixms, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`)
|
return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||||
}
|
}
|
||||||
Vue.prototype.$formatJsDatetime = (jsdate, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
Vue.prototype.$formatJsDatetime = (jsdate, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||||
if (!jsdate || !isDate(jsdate)) return ''
|
if (!jsdate || !isDate(jsdate)) return ''
|
||||||
return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`)
|
return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||||
}
|
}
|
||||||
Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
||||||
var date = addDays(new Date(), daysToAdd)
|
var date = addDays(new Date(), daysToAdd)
|
||||||
@@ -132,8 +132,10 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
|||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(str).then(() => {
|
navigator.clipboard.writeText(str).then(() => {
|
||||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||||
|
resolve(true)
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('Clipboard copy failed', str, err)
|
console.error('Clipboard copy failed', str, err)
|
||||||
|
resolve(false)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
@@ -147,6 +149,7 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
|||||||
document.body.removeChild(el)
|
document.body.removeChild(el)
|
||||||
|
|
||||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
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) => {
|
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => {
|
||||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
if (!libraryItem) return placeholder
|
if (!libraryItem) return placeholder
|
||||||
var media = libraryItem.media
|
const media = libraryItem.media
|
||||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
if (!media?.coverPath || media.coverPath === placeholder) return placeholder
|
||||||
|
|
||||||
// Absolute URL covers (should no longer be used)
|
// Absolute URL covers (should no longer be used)
|
||||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
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}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
},
|
},
|
||||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null, raw = false) => {
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
||||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
if (!libraryItemId) return placeholder
|
if (!libraryItemId) return placeholder
|
||||||
var userToken = rootGetters['user/getToken']
|
const userToken = rootGetters['user/getToken']
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
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) => {
|
getIsBatchSelectingMediaItems: (state) => {
|
||||||
return state.selectedMediaItems.length
|
return state.selectedMediaItems.length
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ export const state = () => ({
|
|||||||
editPodcastModalTab: 'details',
|
editPodcastModalTab: 'details',
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showEReader: false,
|
showEReader: false,
|
||||||
|
ereaderKeepProgress: false,
|
||||||
|
ereaderFileId: null,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
processingBatch: false,
|
processingBatch: false,
|
||||||
previousPath: '/',
|
previousPath: '/',
|
||||||
showExperimentalFeatures: false,
|
|
||||||
bookshelfBookIds: [],
|
bookshelfBookIds: [],
|
||||||
episodeTableEpisodeIds: [],
|
episodeTableEpisodeIds: [],
|
||||||
openModal: null,
|
openModal: null,
|
||||||
@@ -210,8 +211,10 @@ export const mutations = {
|
|||||||
setEditPodcastModalTab(state, tab) {
|
setEditPodcastModalTab(state, tab) {
|
||||||
state.editPodcastModalTab = tab
|
state.editPodcastModalTab = tab
|
||||||
},
|
},
|
||||||
showEReader(state, libraryItem) {
|
showEReader(state, { libraryItem, keepProgress, fileId }) {
|
||||||
state.selectedLibraryItem = libraryItem
|
state.selectedLibraryItem = libraryItem
|
||||||
|
state.ereaderKeepProgress = keepProgress
|
||||||
|
state.ereaderFileId = fileId
|
||||||
|
|
||||||
state.showEReader = true
|
state.showEReader = true
|
||||||
},
|
},
|
||||||
@@ -227,10 +230,6 @@ export const mutations = {
|
|||||||
setProcessingBatch(state, val) {
|
setProcessingBatch(state, val) {
|
||||||
state.processingBatch = val
|
state.processingBatch = val
|
||||||
},
|
},
|
||||||
setExperimentalFeatures(state, val) {
|
|
||||||
state.showExperimentalFeatures = val
|
|
||||||
localStorage.setItem('experimental', val ? 1 : 0)
|
|
||||||
},
|
|
||||||
setOpenModal(state, val) {
|
setOpenModal(state, val) {
|
||||||
state.openModal = val
|
state.openModal = val
|
||||||
},
|
},
|
||||||
|
|||||||
+28
-15
@@ -57,6 +57,9 @@ export const getters = {
|
|||||||
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1
|
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1
|
||||||
return getters.getCurrentLibrarySettings.coverAspectRatio === Constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
return getters.getCurrentLibrarySettings.coverAspectRatio === Constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||||
},
|
},
|
||||||
|
getLibraryIsAudiobooksOnly: (state, getters) => {
|
||||||
|
return !!getters.getCurrentLibrarySettings?.audiobooksOnly
|
||||||
|
},
|
||||||
getCollection: state => id => {
|
getCollection: state => id => {
|
||||||
return state.collections.find(c => c.id === id)
|
return state.collections.find(c => c.id === id)
|
||||||
},
|
},
|
||||||
@@ -231,25 +234,31 @@ export const mutations = {
|
|||||||
setNumUserPlaylists(state, numUserPlaylists) {
|
setNumUserPlaylists(state, numUserPlaylists) {
|
||||||
state.numUserPlaylists = numUserPlaylists
|
state.numUserPlaylists = numUserPlaylists
|
||||||
},
|
},
|
||||||
|
removeSeriesFromFilterData(state, seriesId) {
|
||||||
|
if (!seriesId || !state.filterData) return
|
||||||
|
state.filterData.series = state.filterData.series.filter(se => se.id !== seriesId)
|
||||||
|
},
|
||||||
updateFilterDataWithItem(state, libraryItem) {
|
updateFilterDataWithItem(state, libraryItem) {
|
||||||
if (!libraryItem || !state.filterData) return
|
if (!libraryItem || !state.filterData) return
|
||||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||||
/*
|
/*
|
||||||
var data = {
|
structure of filterData:
|
||||||
|
{
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
series: [],
|
series: [],
|
||||||
narrators: [],
|
narrators: [],
|
||||||
languages: []
|
languages: [],
|
||||||
|
publishers: []
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
var mediaMetadata = libraryItem.media.metadata
|
const mediaMetadata = libraryItem.media.metadata
|
||||||
|
|
||||||
// Add/update book authors
|
// Add/update book authors
|
||||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
if (mediaMetadata.authors?.length) {
|
||||||
mediaMetadata.authors.forEach((author) => {
|
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) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.authors.splice(indexOf, 1, author)
|
state.filterData.authors.splice(indexOf, 1, author)
|
||||||
} else {
|
} else {
|
||||||
@@ -260,9 +269,9 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add/update series
|
// Add/update series
|
||||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
if (mediaMetadata.series?.length) {
|
||||||
mediaMetadata.series.forEach((series) => {
|
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) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
||||||
} else {
|
} else {
|
||||||
@@ -273,7 +282,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add genres
|
// Add genres
|
||||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
if (mediaMetadata.genres?.length) {
|
||||||
mediaMetadata.genres.forEach((genre) => {
|
mediaMetadata.genres.forEach((genre) => {
|
||||||
if (!state.filterData.genres.includes(genre)) {
|
if (!state.filterData.genres.includes(genre)) {
|
||||||
state.filterData.genres.push(genre)
|
state.filterData.genres.push(genre)
|
||||||
@@ -283,7 +292,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add tags
|
// Add tags
|
||||||
if (libraryItem.media.tags && libraryItem.media.tags.length) {
|
if (libraryItem.media.tags?.length) {
|
||||||
libraryItem.media.tags.forEach((tag) => {
|
libraryItem.media.tags.forEach((tag) => {
|
||||||
if (!state.filterData.tags.includes(tag)) {
|
if (!state.filterData.tags.includes(tag)) {
|
||||||
state.filterData.tags.push(tag)
|
state.filterData.tags.push(tag)
|
||||||
@@ -293,7 +302,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add narrators
|
// Add narrators
|
||||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
if (mediaMetadata.narrators?.length) {
|
||||||
mediaMetadata.narrators.forEach((narrator) => {
|
mediaMetadata.narrators.forEach((narrator) => {
|
||||||
if (!state.filterData.narrators.includes(narrator)) {
|
if (!state.filterData.narrators.includes(narrator)) {
|
||||||
state.filterData.narrators.push(narrator)
|
state.filterData.narrators.push(narrator)
|
||||||
@@ -302,12 +311,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
|
// Add language
|
||||||
if (mediaMetadata.language) {
|
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
|
||||||
if (!state.filterData.languages.includes(mediaMetadata.language)) {
|
state.filterData.languages.push(mediaMetadata.language)
|
||||||
state.filterData.languages.push(mediaMetadata.language)
|
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setCollections(state, collections) {
|
setCollections(state, collections) {
|
||||||
|
|||||||
@@ -33,22 +33,22 @@ export const getters = {
|
|||||||
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] : null
|
return state.settings?.[key] || null
|
||||||
},
|
},
|
||||||
getUserCanUpdate: (state) => {
|
getUserCanUpdate: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.update : false
|
return !!state.user?.permissions?.update
|
||||||
},
|
},
|
||||||
getUserCanDelete: (state) => {
|
getUserCanDelete: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.delete : false
|
return !!state.user?.permissions?.delete
|
||||||
},
|
},
|
||||||
getUserCanDownload: (state) => {
|
getUserCanDownload: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
return !!state.user?.permissions?.download
|
||||||
},
|
},
|
||||||
getUserCanUpload: (state) => {
|
getUserCanUpload: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.upload : false
|
return !!state.user?.permissions?.upload
|
||||||
},
|
},
|
||||||
getUserCanAccessAllLibraries: (state) => {
|
getUserCanAccessAllLibraries: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.accessAllLibraries : false
|
return !!state.user?.permissions?.accessAllLibraries
|
||||||
},
|
},
|
||||||
getLibrariesAccessible: (state, getters) => {
|
getLibrariesAccessible: (state, getters) => {
|
||||||
if (!state.user) return []
|
if (!state.user) return []
|
||||||
@@ -80,7 +80,7 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.title'
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
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()
|
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
settingsUpdate.filterBy = 'all'
|
settingsUpdate.filterBy = 'all'
|
||||||
|
|||||||
+63
-24
@@ -3,7 +3,7 @@
|
|||||||
"ButtonAddChapters": "Kapitel hinzufügen",
|
"ButtonAddChapters": "Kapitel hinzufügen",
|
||||||
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
||||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||||
"ButtonApply": "Anwenden",
|
"ButtonApply": "Übernehmen",
|
||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
"ButtonAuthors": "Autoren",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Abbrechen",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOpenManager": "Manager öffnen",
|
"ButtonOpenManager": "Manager öffnen",
|
||||||
@@ -98,10 +98,12 @@
|
|||||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Warteschlange",
|
"HeaderDownloadQueue": "Download Warteschlange",
|
||||||
|
"HeaderEbookFiles": "E-Book Dateien",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Einstellungen",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderEReaderDevices": "E-Reader Devices",
|
"HeaderEreaderDevices": "Ereader Geräte",
|
||||||
|
"HeaderEreaderSettings": "Ereader Einstellungen",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
@@ -136,6 +138,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
|
"HeaderRSSFeeds": "RSS Feeds",
|
||||||
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
||||||
"HeaderSchedule": "Zeitplan",
|
"HeaderSchedule": "Zeitplan",
|
||||||
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
||||||
@@ -153,6 +156,7 @@
|
|||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
||||||
|
"HeaderTableOfContents": "Inhaltsverzeichnis",
|
||||||
"HeaderTools": "Werkzeuge",
|
"HeaderTools": "Werkzeuge",
|
||||||
"HeaderUpdateAccount": "Konto aktualisieren",
|
"HeaderUpdateAccount": "Konto aktualisieren",
|
||||||
"HeaderUpdateAuthor": "Autor aktualisieren",
|
"HeaderUpdateAuthor": "Autor aktualisieren",
|
||||||
@@ -192,17 +196,18 @@
|
|||||||
"LabelBooks": "Bücher",
|
"LabelBooks": "Bücher",
|
||||||
"LabelChangePassword": "Passwort ändern",
|
"LabelChangePassword": "Passwort ändern",
|
||||||
"LabelChannels": "Kanäle",
|
"LabelChannels": "Kanäle",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Kapitel",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
"LabelClosePlayer": "Player schließen",
|
"LabelClosePlayer": "Player schließen",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Serien zusammenfassen",
|
"LabelCollapseSeries": "Serien zusammenfassen",
|
||||||
|
"LabelCollection": "Sammlung",
|
||||||
"LabelCollections": "Sammlungen",
|
"LabelCollections": "Sammlungen",
|
||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
"LabelConfirmPassword": "Passwort bestätigen",
|
"LabelConfirmPassword": "Passwort bestätigen",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
"LabelContinueReading": "Continue Reading",
|
"LabelContinueReading": "Lesen fortsetzen",
|
||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
@@ -219,15 +224,19 @@
|
|||||||
"LabelDirectory": "Verzeichnis",
|
"LabelDirectory": "Verzeichnis",
|
||||||
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
||||||
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
||||||
|
"LabelDiscover": "Finden",
|
||||||
"LabelDownload": "Herunterladen",
|
"LabelDownload": "Herunterladen",
|
||||||
|
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "E-Book",
|
||||||
|
"LabelEbooks": "E-Books",
|
||||||
"LabelEdit": "Bearbeiten",
|
"LabelEdit": "Bearbeiten",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "Von Address",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"LabelEmailSettingsSecure": "Sicherheit",
|
||||||
"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)",
|
"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",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
@@ -238,7 +247,7 @@
|
|||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
"LabelFileBirthtime": "Datei erstellt",
|
||||||
"LabelFileModified": "Datei geändert",
|
"LabelFileModified": "Datei geändert",
|
||||||
"LabelFilename": "Dateiname",
|
"LabelFilename": "Dateiname",
|
||||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||||
@@ -246,10 +255,13 @@
|
|||||||
"LabelFinished": "beendet",
|
"LabelFinished": "beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFolders": "Verzeichnisse",
|
"LabelFolders": "Verzeichnisse",
|
||||||
|
"LabelFontScale": "Schriftgröße",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
|
"LabelHasEbook": "mit E-Book",
|
||||||
|
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
"LabelIcon": "Symbol",
|
"LabelIcon": "Symbol",
|
||||||
@@ -266,7 +278,7 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Umkehren",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
@@ -275,12 +287,16 @@
|
|||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
"LabelLastTime": "Letztes Mal",
|
"LabelLastTime": "Letztes Mal",
|
||||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutSinglePage": "Eine Seite",
|
||||||
|
"LabelLayoutSplitPage": "Geteilte Seite",
|
||||||
"LabelLess": "Weniger",
|
"LabelLess": "Weniger",
|
||||||
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
||||||
"LabelLibrary": "Bibliothek",
|
"LabelLibrary": "Bibliothek",
|
||||||
"LabelLibraryItem": "Bibliothekseintrag",
|
"LabelLibraryItem": "Bibliothekseintrag",
|
||||||
"LabelLibraryName": "Bibliotheksname",
|
"LabelLibraryName": "Bibliotheksname",
|
||||||
"LabelLimit": "Begrenzung",
|
"LabelLimit": "Begrenzung",
|
||||||
|
"LabelLineSpacing": "Zeilenabstand",
|
||||||
"LabelListenAgain": "Erneut anhören",
|
"LabelListenAgain": "Erneut anhören",
|
||||||
"LabelLogLevelDebug": "Fehlersuche",
|
"LabelLogLevelDebug": "Fehlersuche",
|
||||||
"LabelLogLevelInfo": "Informationen",
|
"LabelLogLevelInfo": "Informationen",
|
||||||
@@ -295,7 +311,7 @@
|
|||||||
"LabelMissing": "Fehlend",
|
"LabelMissing": "Fehlend",
|
||||||
"LabelMissingParts": "Fehlende Teile",
|
"LabelMissingParts": "Fehlende Teile",
|
||||||
"LabelMore": "Mehr",
|
"LabelMore": "Mehr",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Mehr Info",
|
||||||
"LabelName": "Name",
|
"LabelName": "Name",
|
||||||
"LabelNarrator": "Erzähler",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
@@ -305,6 +321,7 @@
|
|||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||||
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||||
|
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Hinweise",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -339,12 +356,15 @@
|
|||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"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",
|
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||||
|
"LabelPrimaryEbook": "Haupt-E-Book",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelRead": "Lesen",
|
||||||
|
"LabelReadAgain": "Nocheinmal Lesen",
|
||||||
|
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
@@ -361,23 +381,32 @@
|
|||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||||
"LabelSeason": "Staffel",
|
"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",
|
"LabelSequence": "Reihenfolge",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"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",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||||
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||||
"LabelSettingsDateFormat": "Datumsformat",
|
"LabelSettingsDateFormat": "Datumsformat",
|
||||||
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
||||||
"LabelSettingsDisableWatcherHelp": "Deaktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
"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",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"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)",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr 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",
|
"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",
|
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||||
@@ -404,6 +433,7 @@
|
|||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
|
"LabelSlug": "URL Teil",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Gestartet",
|
"LabelStarted": "Gestartet",
|
||||||
"LabelStartedAt": "Gestartet am",
|
"LabelStartedAt": "Gestartet am",
|
||||||
@@ -430,6 +460,9 @@
|
|||||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||||
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
||||||
"LabelTasks": "Laufende Aufgaben",
|
"LabelTasks": "Laufende Aufgaben",
|
||||||
|
"LabelTheme": "Theme",
|
||||||
|
"LabelThemeDark": "Dark",
|
||||||
|
"LabelThemeLight": "Light",
|
||||||
"LabelTimeBase": "Basiszeit",
|
"LabelTimeBase": "Basiszeit",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
@@ -448,6 +481,7 @@
|
|||||||
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
||||||
"LabelTracks": "Dateien",
|
"LabelTracks": "Dateien",
|
||||||
"LabelTracksMultiTrack": "Mehrfachdatei",
|
"LabelTracksMultiTrack": "Mehrfachdatei",
|
||||||
|
"LabelTracksNone": "Keine Dateien",
|
||||||
"LabelTracksSingleTrack": "Einzeldatei",
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Ungekürzt",
|
"LabelUnabridged": "Ungekürzt",
|
||||||
@@ -468,7 +502,7 @@
|
|||||||
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
||||||
"LabelViewChapters": "Kapitel anzeigen",
|
"LabelViewChapters": "Kapitel anzeigen",
|
||||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
@@ -488,10 +522,14 @@
|
|||||||
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"MessageCheckingCron": "Überprüfe Cron...",
|
"MessageCheckingCron": "Überprüfe Cron...",
|
||||||
|
"MessageConfirmCloseFeed": "Sind Sie sicher, dass Sie diesen Feed schließen wollen?",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
|
"MessageConfirmDeleteFile": "Es wird die Datei vom System löschen. Sind Sie sicher?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
|
"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?",
|
"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?",
|
"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?",
|
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
||||||
@@ -506,7 +544,7 @@
|
|||||||
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
"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.",
|
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||||
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
"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",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
@@ -525,9 +563,11 @@
|
|||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
"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",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "Es wird versucht die Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen um leere Details und das Titelbild auszufüllen. Vorhandene Details werden nicht überschrieben.",
|
||||||
"MessageNoAudioTracks": "Keine Audiodateien",
|
"MessageNoAudioTracks": "Keine Audiodateien",
|
||||||
"MessageNoAuthors": "Keine Autoren",
|
"MessageNoAuthors": "Keine Autoren",
|
||||||
"MessageNoBackups": "Keine Sicherungen",
|
"MessageNoBackups": "Keine Sicherungen",
|
||||||
@@ -563,9 +603,8 @@
|
|||||||
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||||
"MessagePlayChapter": "Kapitelanfang anhören",
|
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||||
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
||||||
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
|
||||||
"MessageRemoveChapter": "Kapitel löschen",
|
"MessageRemoveChapter": "Kapitel löschen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
||||||
@@ -661,8 +700,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
|
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
|
|||||||
@@ -98,10 +98,12 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodes",
|
"HeaderEpisodes": "Episodes",
|
||||||
"HeaderEReaderDevices": "E-Reader Devices",
|
"HeaderEreaderDevices": "Ereader Devices",
|
||||||
|
"HeaderEreaderSettings": "Ereader Settings",
|
||||||
"HeaderFiles": "Files",
|
"HeaderFiles": "Files",
|
||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
@@ -136,6 +138,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
||||||
|
"HeaderRSSFeeds": "RSS Feeds",
|
||||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||||
"HeaderSchedule": "Schedule",
|
"HeaderSchedule": "Schedule",
|
||||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||||
@@ -153,6 +156,7 @@
|
|||||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Authors",
|
"HeaderStatsTop10Authors": "Top 10 Authors",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||||
|
"HeaderTableOfContents": "Table of Contents",
|
||||||
"HeaderTools": "Tools",
|
"HeaderTools": "Tools",
|
||||||
"HeaderUpdateAccount": "Update Account",
|
"HeaderUpdateAccount": "Update Account",
|
||||||
"HeaderUpdateAuthor": "Update Author",
|
"HeaderUpdateAuthor": "Update Author",
|
||||||
@@ -198,6 +202,7 @@
|
|||||||
"LabelClosePlayer": "Close player",
|
"LabelClosePlayer": "Close player",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Collapse Series",
|
"LabelCollapseSeries": "Collapse Series",
|
||||||
|
"LabelCollection": "Collection",
|
||||||
"LabelCollections": "Collections",
|
"LabelCollections": "Collections",
|
||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
@@ -219,15 +224,19 @@
|
|||||||
"LabelDirectory": "Directory",
|
"LabelDirectory": "Directory",
|
||||||
"LabelDiscFromFilename": "Disc from Filename",
|
"LabelDiscFromFilename": "Disc from Filename",
|
||||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||||
|
"LabelDiscover": "Discover",
|
||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
|
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||||
"LabelDuration": "Duration",
|
"LabelDuration": "Duration",
|
||||||
"LabelDurationFound": "Duration found:",
|
"LabelDurationFound": "Duration found:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEdit": "Edit",
|
"LabelEdit": "Edit",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"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)",
|
"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",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Enable",
|
"LabelEnable": "Enable",
|
||||||
"LabelEnd": "End",
|
"LabelEnd": "End",
|
||||||
@@ -246,10 +255,13 @@
|
|||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folders",
|
"LabelFolders": "Folders",
|
||||||
|
"LabelFontScale": "Font scale",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Hard delete file",
|
"LabelHardDeleteFile": "Hard delete file",
|
||||||
|
"LabelHasEbook": "Has ebook",
|
||||||
|
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hour",
|
"LabelHour": "Hour",
|
||||||
"LabelIcon": "Icon",
|
"LabelIcon": "Icon",
|
||||||
@@ -275,12 +287,16 @@
|
|||||||
"LabelLastSeen": "Last Seen",
|
"LabelLastSeen": "Last Seen",
|
||||||
"LabelLastTime": "Last Time",
|
"LabelLastTime": "Last Time",
|
||||||
"LabelLastUpdate": "Last Update",
|
"LabelLastUpdate": "Last Update",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
|
"LabelLayoutSplitPage": "Split page",
|
||||||
"LabelLess": "Less",
|
"LabelLess": "Less",
|
||||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||||
"LabelLibrary": "Library",
|
"LabelLibrary": "Library",
|
||||||
"LabelLibraryItem": "Library Item",
|
"LabelLibraryItem": "Library Item",
|
||||||
"LabelLibraryName": "Library Name",
|
"LabelLibraryName": "Library Name",
|
||||||
"LabelLimit": "Limit",
|
"LabelLimit": "Limit",
|
||||||
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
@@ -305,6 +321,7 @@
|
|||||||
"LabelNewPassword": "New Password",
|
"LabelNewPassword": "New Password",
|
||||||
"LabelNextBackupDate": "Next backup date",
|
"LabelNextBackupDate": "Next backup date",
|
||||||
"LabelNextScheduledRun": "Next scheduled run",
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
|
"LabelNoEpisodesSelected": "No episodes selected",
|
||||||
"LabelNotes": "Notes",
|
"LabelNotes": "Notes",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -339,12 +356,15 @@
|
|||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
|
"LabelPrimaryEbook": "Primary ebook",
|
||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelRead": "Read",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Read Again",
|
||||||
|
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -361,23 +381,32 @@
|
|||||||
"LabelSearchTitle": "Search Title",
|
"LabelSearchTitle": "Search Title",
|
||||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
|
"LabelSelectAllEpisodes": "Select all episodes",
|
||||||
|
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sequence",
|
"LabelSequence": "Sequence",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
"LabelSeriesProgress": "Series Progress",
|
"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",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||||
"LabelSettingsDateFormat": "Date Format",
|
"LabelSettingsDateFormat": "Date Format",
|
||||||
"LabelSettingsDisableWatcher": "Disable Watcher",
|
"LabelSettingsDisableWatcher": "Disable Watcher",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
|
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
|
||||||
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsEnableEReader": "Enable e-reader for all users",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"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)",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimental features",
|
"LabelSettingsExperimentalFeatures": "Experimental features",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
||||||
"LabelSettingsFindCovers": "Find covers",
|
"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",
|
"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",
|
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
|
||||||
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
|
||||||
@@ -404,6 +433,7 @@
|
|||||||
"LabelShowAll": "Show All",
|
"LabelShowAll": "Show All",
|
||||||
"LabelSize": "Size",
|
"LabelSize": "Size",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Started",
|
"LabelStarted": "Started",
|
||||||
"LabelStartedAt": "Started At",
|
"LabelStartedAt": "Started At",
|
||||||
@@ -430,6 +460,9 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTheme": "Theme",
|
||||||
|
"LabelThemeDark": "Dark",
|
||||||
|
"LabelThemeLight": "Light",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
"LabelTimeListenedToday": "Time Listened Today",
|
"LabelTimeListenedToday": "Time Listened Today",
|
||||||
@@ -448,6 +481,7 @@
|
|||||||
"LabelTrackFromMetadata": "Track from Metadata",
|
"LabelTrackFromMetadata": "Track from Metadata",
|
||||||
"LabelTracks": "Tracks",
|
"LabelTracks": "Tracks",
|
||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
|
"LabelTracksNone": "No tracks",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
@@ -488,10 +522,14 @@
|
|||||||
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
||||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||||
"MessageCheckingCron": "Checking cron...",
|
"MessageCheckingCron": "Checking cron...",
|
||||||
|
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
"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}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"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?",
|
"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?",
|
"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?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -525,6 +563,8 @@
|
|||||||
"MessageM4BFailed": "M4B Failed!",
|
"MessageM4BFailed": "M4B Failed!",
|
||||||
"MessageM4BFinished": "M4B Finished!",
|
"MessageM4BFinished": "M4B Finished!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"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",
|
"MessageMarkAsFinished": "Mark as Finished",
|
||||||
"MessageMarkAsNotFinished": "Mark as Not 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.",
|
"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 +605,6 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
||||||
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
|
||||||
"MessageRemoveChapter": "Remove chapter",
|
"MessageRemoveChapter": "Remove chapter",
|
||||||
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
||||||
|
|||||||
+43
-4
@@ -98,10 +98,12 @@
|
|||||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||||
"HeaderDetails": "Detalles",
|
"HeaderDetails": "Detalles",
|
||||||
"HeaderDownloadQueue": "Lista de Descarga",
|
"HeaderDownloadQueue": "Lista de Descarga",
|
||||||
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodios",
|
"HeaderEpisodes": "Episodios",
|
||||||
"HeaderEReaderDevices": "E-Reader Devices",
|
"HeaderEreaderDevices": "Ereader Devices",
|
||||||
|
"HeaderEreaderSettings": "Ereader Settings",
|
||||||
"HeaderFiles": "Elemento",
|
"HeaderFiles": "Elemento",
|
||||||
"HeaderFindChapters": "Buscar Capitulo",
|
"HeaderFindChapters": "Buscar Capitulo",
|
||||||
"HeaderIgnoredFiles": "Ignorar Elemento",
|
"HeaderIgnoredFiles": "Ignorar Elemento",
|
||||||
@@ -136,6 +138,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Remover {0} Episodios",
|
"HeaderRemoveEpisodes": "Remover {0} Episodios",
|
||||||
"HeaderRSSFeedGeneral": "Detalles RSS",
|
"HeaderRSSFeedGeneral": "Detalles RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
||||||
|
"HeaderRSSFeeds": "RSS Feeds",
|
||||||
"HeaderSavedMediaProgress": "Guardar Progreso de multimedia",
|
"HeaderSavedMediaProgress": "Guardar Progreso de multimedia",
|
||||||
"HeaderSchedule": "Horario",
|
"HeaderSchedule": "Horario",
|
||||||
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
||||||
@@ -153,6 +156,7 @@
|
|||||||
"HeaderStatsRecentSessions": "Sesiones Recientes",
|
"HeaderStatsRecentSessions": "Sesiones Recientes",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autores",
|
"HeaderStatsTop10Authors": "Top 10 Autores",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Géneros",
|
"HeaderStatsTop5Genres": "Top 5 Géneros",
|
||||||
|
"HeaderTableOfContents": "Table of Contents",
|
||||||
"HeaderTools": "Herramientas",
|
"HeaderTools": "Herramientas",
|
||||||
"HeaderUpdateAccount": "Actualizar Cuenta",
|
"HeaderUpdateAccount": "Actualizar Cuenta",
|
||||||
"HeaderUpdateAuthor": "Actualizar Autor",
|
"HeaderUpdateAuthor": "Actualizar Autor",
|
||||||
@@ -198,6 +202,7 @@
|
|||||||
"LabelClosePlayer": "Close player",
|
"LabelClosePlayer": "Close player",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Colapsar Series",
|
"LabelCollapseSeries": "Colapsar Series",
|
||||||
|
"LabelCollection": "Collection",
|
||||||
"LabelCollections": "Colecciones",
|
"LabelCollections": "Colecciones",
|
||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Confirmar Contraseña",
|
"LabelConfirmPassword": "Confirmar Contraseña",
|
||||||
@@ -219,15 +224,19 @@
|
|||||||
"LabelDirectory": "Directorio",
|
"LabelDirectory": "Directorio",
|
||||||
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
|
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
|
||||||
"LabelDiscFromMetadata": "Disco a partir de Metadata",
|
"LabelDiscFromMetadata": "Disco a partir de Metadata",
|
||||||
|
"LabelDiscover": "Discover",
|
||||||
"LabelDownload": "Descargar",
|
"LabelDownload": "Descargar",
|
||||||
|
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"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)",
|
"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",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
@@ -246,10 +255,13 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
|
"LabelFontScale": "Font scale",
|
||||||
"LabelFormat": "Formato",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
|
"LabelHasEbook": "Has ebook",
|
||||||
|
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hora",
|
"LabelHour": "Hora",
|
||||||
"LabelIcon": "Icono",
|
"LabelIcon": "Icono",
|
||||||
@@ -275,12 +287,16 @@
|
|||||||
"LabelLastSeen": "Ultima Vez Visto",
|
"LabelLastSeen": "Ultima Vez Visto",
|
||||||
"LabelLastTime": "Ultima Vez",
|
"LabelLastTime": "Ultima Vez",
|
||||||
"LabelLastUpdate": "Ultima Actualización",
|
"LabelLastUpdate": "Ultima Actualización",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
|
"LabelLayoutSplitPage": "Split page",
|
||||||
"LabelLess": "Menos",
|
"LabelLess": "Menos",
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
||||||
"LabelLibrary": "Biblioteca",
|
"LabelLibrary": "Biblioteca",
|
||||||
"LabelLibraryItem": "Elemento de Biblioteca",
|
"LabelLibraryItem": "Elemento de Biblioteca",
|
||||||
"LabelLibraryName": "Nombre de Biblioteca",
|
"LabelLibraryName": "Nombre de Biblioteca",
|
||||||
"LabelLimit": "Limites",
|
"LabelLimit": "Limites",
|
||||||
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Escuchar Otra Vez",
|
"LabelListenAgain": "Escuchar Otra Vez",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
@@ -305,6 +321,7 @@
|
|||||||
"LabelNewPassword": "Nueva Contraseña",
|
"LabelNewPassword": "Nueva Contraseña",
|
||||||
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
||||||
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
||||||
|
"LabelNoEpisodesSelected": "No episodes selected",
|
||||||
"LabelNotes": "Notas",
|
"LabelNotes": "Notas",
|
||||||
"LabelNotFinished": "No Terminado",
|
"LabelNotFinished": "No Terminado",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -339,12 +356,15 @@
|
|||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
"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",
|
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
|
||||||
|
"LabelPrimaryEbook": "Primary ebook",
|
||||||
"LabelProgress": "Progreso",
|
"LabelProgress": "Progreso",
|
||||||
"LabelProvider": "Proveedor",
|
"LabelProvider": "Proveedor",
|
||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublisher": "Editor",
|
"LabelPublisher": "Editor",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
|
"LabelRead": "Read",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Read Again",
|
||||||
|
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
||||||
"LabelRecentlyAdded": "Agregado Reciente",
|
"LabelRecentlyAdded": "Agregado Reciente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
@@ -361,23 +381,32 @@
|
|||||||
"LabelSearchTitle": "Buscar Titulo",
|
"LabelSearchTitle": "Buscar Titulo",
|
||||||
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
|
"LabelSelectAllEpisodes": "Select all episodes",
|
||||||
|
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Secuencia",
|
"LabelSequence": "Secuencia",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Nombre de la Serie",
|
"LabelSeriesName": "Nombre de la Serie",
|
||||||
"LabelSeriesProgress": "Progreso 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",
|
"LabelSettingsBookshelfViewHelp": "Diseño Skeumorphic con Estantes de Madera",
|
||||||
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
|
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
|
||||||
"LabelSettingsDateFormat": "Formato de Fecha",
|
"LabelSettingsDateFormat": "Formato de Fecha",
|
||||||
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
|
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
|
"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",
|
"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",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"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)",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
"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.",
|
"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",
|
"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",
|
"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",
|
"LabelSettingsHomePageBookshelfView": "La pagina de inicio usa la vista de librero",
|
||||||
"LabelSettingsLibraryBookshelfView": "La biblioteca usa la vista de librero",
|
"LabelSettingsLibraryBookshelfView": "La biblioteca usa la vista de librero",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Usar Markers de multimedia en Overdrive para estos capítulos",
|
"LabelSettingsOverdriveMediaMarkers": "Usar Markers de multimedia en Overdrive para estos capítulos",
|
||||||
@@ -404,6 +433,7 @@
|
|||||||
"LabelShowAll": "Mostrar Todos",
|
"LabelShowAll": "Mostrar Todos",
|
||||||
"LabelSize": "Tamaño",
|
"LabelSize": "Tamaño",
|
||||||
"LabelSleepTimer": "Temporizador para Dormir",
|
"LabelSleepTimer": "Temporizador para Dormir",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
"LabelStart": "Iniciar",
|
"LabelStart": "Iniciar",
|
||||||
"LabelStarted": "Indiciado",
|
"LabelStarted": "Indiciado",
|
||||||
"LabelStartedAt": "Iniciado En",
|
"LabelStartedAt": "Iniciado En",
|
||||||
@@ -430,6 +460,9 @@
|
|||||||
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tareas Corriendo",
|
"LabelTasks": "Tareas Corriendo",
|
||||||
|
"LabelTheme": "Theme",
|
||||||
|
"LabelThemeDark": "Dark",
|
||||||
|
"LabelThemeLight": "Light",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
||||||
@@ -448,6 +481,7 @@
|
|||||||
"LabelTrackFromMetadata": "Pista desde Metadata",
|
"LabelTrackFromMetadata": "Pista desde Metadata",
|
||||||
"LabelTracks": "Pistas",
|
"LabelTracks": "Pistas",
|
||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
|
"LabelTracksNone": "No tracks",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
@@ -488,10 +522,14 @@
|
|||||||
"MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válida debe ser mayor o igual que la hora de inicio del capítulo anterior",
|
"MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válida debe ser mayor o igual que la hora de inicio del capítulo anterior",
|
||||||
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
|
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
|
||||||
"MessageCheckingCron": "Checking cron...",
|
"MessageCheckingCron": "Checking cron...",
|
||||||
|
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||||
"MessageConfirmDeleteBackup": "Esta seguro que desea eliminar el respaldo {0}?",
|
"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}\"?",
|
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
||||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
"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?",
|
"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?",
|
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||||
@@ -525,6 +563,8 @@
|
|||||||
"MessageM4BFailed": "M4B Fallo!",
|
"MessageM4BFailed": "M4B Fallo!",
|
||||||
"MessageM4BFinished": "M4B Terminado!",
|
"MessageM4BFinished": "M4B Terminado!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"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",
|
"MessageMarkAsFinished": "Marcar como Terminado",
|
||||||
"MessageMarkAsNotFinished": "Marcar como No 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.",
|
"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 +605,6 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Crear lista de reproducción a partir de colección",
|
"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",
|
"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.",
|
"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",
|
"MessageRemoveChapter": "Remover capítulos",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
||||||
|
|||||||
+80
-41
@@ -55,7 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Supprimer tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||||
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
"ButtonRemoveFromContinueReading": "Ne plus continuer à lire",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
||||||
"ButtonReScan": "Nouvelle analyse",
|
"ButtonReScan": "Nouvelle analyse",
|
||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"HeaderAdvanced": "Avancé",
|
"HeaderAdvanced": "Avancé",
|
||||||
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
|
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
|
||||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||||
"HeaderAudioTracks": "Pistes zudio",
|
"HeaderAudioTracks": "Pistes audio",
|
||||||
"HeaderBackups": "Sauvegardes",
|
"HeaderBackups": "Sauvegardes",
|
||||||
"HeaderChangePassword": "Modifier le mot de passe",
|
"HeaderChangePassword": "Modifier le mot de passe",
|
||||||
"HeaderChapters": "Chapitres",
|
"HeaderChapters": "Chapitres",
|
||||||
@@ -95,13 +95,15 @@
|
|||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
"HeaderCollectionItems": "Entrées de la Collection",
|
"HeaderCollectionItems": "Entrées de la Collection",
|
||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
"HeaderCurrentDownloads": "File d’attente de téléchargement",
|
"HeaderCurrentDownloads": "Téléchargements en cours",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
"HeaderDownloadQueue": "Queue de téléchargement",
|
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEbookFiles": "Fichier des livres numériques",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmail": "Courriels",
|
||||||
|
"HeaderEmailSettings": "Configuration des courriels",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
"HeaderEReaderDevices": "E-Reader Devices",
|
"HeaderEreaderDevices": "Lecteur de livres numériques",
|
||||||
|
"HeaderEreaderSettings": "Options Ereader",
|
||||||
"HeaderFiles": "Fichiers",
|
"HeaderFiles": "Fichiers",
|
||||||
"HeaderFindChapters": "Trouver les chapitres",
|
"HeaderFindChapters": "Trouver les chapitres",
|
||||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||||
@@ -136,6 +138,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
||||||
"HeaderRSSFeedGeneral": "Détails de flux RSS",
|
"HeaderRSSFeedGeneral": "Détails de flux RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
|
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
|
||||||
|
"HeaderRSSFeeds": "RSS Feeds",
|
||||||
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
||||||
"HeaderSchedule": "Programmation",
|
"HeaderSchedule": "Programmation",
|
||||||
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
||||||
@@ -153,6 +156,7 @@
|
|||||||
"HeaderStatsRecentSessions": "Sessions récentes",
|
"HeaderStatsRecentSessions": "Sessions récentes",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||||
|
"HeaderTableOfContents": "Table des matières",
|
||||||
"HeaderTools": "Outils",
|
"HeaderTools": "Outils",
|
||||||
"HeaderUpdateAccount": "Mettre à jour le compte",
|
"HeaderUpdateAccount": "Mettre à jour le compte",
|
||||||
"HeaderUpdateAuthor": "Mettre à jour l’auteur",
|
"HeaderUpdateAuthor": "Mettre à jour l’auteur",
|
||||||
@@ -198,11 +202,12 @@
|
|||||||
"LabelClosePlayer": "Fermer le lecteur",
|
"LabelClosePlayer": "Fermer le lecteur",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Réduire les séries",
|
"LabelCollapseSeries": "Réduire les séries",
|
||||||
|
"LabelCollection": "Collection",
|
||||||
"LabelCollections": "Collections",
|
"LabelCollections": "Collections",
|
||||||
"LabelComplete": "Complet",
|
"LabelComplete": "Complet",
|
||||||
"LabelConfirmPassword": "Confirmer le mot de passe",
|
"LabelConfirmPassword": "Confirmer le mot de passe",
|
||||||
"LabelContinueListening": "Continuer la lecture",
|
"LabelContinueListening": "Continuer la lecture",
|
||||||
"LabelContinueReading": "Continue Reading",
|
"LabelContinueReading": "Continuer la lecture",
|
||||||
"LabelContinueSeries": "Continuer la série",
|
"LabelContinueSeries": "Continuer la série",
|
||||||
"LabelCover": "Couverture",
|
"LabelCover": "Couverture",
|
||||||
"LabelCoverImageURL": "URL vers l’image de couverture",
|
"LabelCoverImageURL": "URL vers l’image de couverture",
|
||||||
@@ -219,15 +224,19 @@
|
|||||||
"LabelDirectory": "Répertoire",
|
"LabelDirectory": "Répertoire",
|
||||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||||
|
"LabelDiscover": "Discover",
|
||||||
"LabelDownload": "Téléchargement",
|
"LabelDownload": "Téléchargement",
|
||||||
|
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
||||||
"LabelDuration": "Durée",
|
"LabelDuration": "Durée",
|
||||||
"LabelDurationFound": "Durée trouvée :",
|
"LabelDurationFound": "Durée trouvée :",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Livre numérique",
|
||||||
|
"LabelEbooks": "Livres numériques",
|
||||||
"LabelEdit": "Modifier",
|
"LabelEdit": "Modifier",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Courriel",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"LabelEmailSettingsSecure": "Sécurisé",
|
||||||
"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)",
|
"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",
|
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||||
"LabelEnable": "Activer",
|
"LabelEnable": "Activer",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
@@ -236,9 +245,9 @@
|
|||||||
"LabelEpisodeType": "Type de l’épisode",
|
"LabelEpisodeType": "Type de l’épisode",
|
||||||
"LabelExample": "Exemple",
|
"LabelExample": "Exemple",
|
||||||
"LabelExplicit": "Restriction",
|
"LabelExplicit": "Restriction",
|
||||||
"LabelFeedURL": "URL deu flux",
|
"LabelFeedURL": "URL du flux",
|
||||||
"LabelFile": "Fichier",
|
"LabelFile": "Fichier",
|
||||||
"LabelFileBirthtime": "Creation du fichier",
|
"LabelFileBirthtime": "Création du fichier",
|
||||||
"LabelFileModified": "Modification du fichier",
|
"LabelFileModified": "Modification du fichier",
|
||||||
"LabelFilename": "Nom de fichier",
|
"LabelFilename": "Nom de fichier",
|
||||||
"LabelFilterByUser": "Filtrer par l’utilisateur",
|
"LabelFilterByUser": "Filtrer par l’utilisateur",
|
||||||
@@ -246,17 +255,20 @@
|
|||||||
"LabelFinished": "Fini(e)",
|
"LabelFinished": "Fini(e)",
|
||||||
"LabelFolder": "Dossier",
|
"LabelFolder": "Dossier",
|
||||||
"LabelFolders": "Dossiers",
|
"LabelFolders": "Dossiers",
|
||||||
|
"LabelFontScale": "Taille de la police de caractère",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Suppression du fichier",
|
"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",
|
"LabelHour": "Heure",
|
||||||
"LabelIcon": "Icone",
|
"LabelIcon": "Icone",
|
||||||
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
||||||
"LabelIncomplete": "Incomplet",
|
"LabelIncomplete": "Incomplet",
|
||||||
"LabelInProgress": "En cours",
|
"LabelInProgress": "En cours",
|
||||||
"LabelInterval": "Interval",
|
"LabelInterval": "Intervalle",
|
||||||
"LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé",
|
"LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé",
|
||||||
"LabelIntervalEvery12Hours": "Toutes les 12 heures",
|
"LabelIntervalEvery12Hours": "Toutes les 12 heures",
|
||||||
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
|
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
|
||||||
@@ -266,7 +278,7 @@
|
|||||||
"LabelIntervalEveryDay": "Tous les jours",
|
"LabelIntervalEveryDay": "Tous les jours",
|
||||||
"LabelIntervalEveryHour": "Toutes les heures",
|
"LabelIntervalEveryHour": "Toutes les heures",
|
||||||
"LabelInvalidParts": "Parties invalides",
|
"LabelInvalidParts": "Parties invalides",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Inverser",
|
||||||
"LabelItem": "Article",
|
"LabelItem": "Article",
|
||||||
"LabelLanguage": "Langue",
|
"LabelLanguage": "Langue",
|
||||||
"LabelLanguageDefaultServer": "Langue par défaut",
|
"LabelLanguageDefaultServer": "Langue par défaut",
|
||||||
@@ -275,12 +287,16 @@
|
|||||||
"LabelLastSeen": "Vu dernièrement",
|
"LabelLastSeen": "Vu dernièrement",
|
||||||
"LabelLastTime": "Progression",
|
"LabelLastTime": "Progression",
|
||||||
"LabelLastUpdate": "Dernière mise à jour",
|
"LabelLastUpdate": "Dernière mise à jour",
|
||||||
|
"LabelLayout": "Disposition",
|
||||||
|
"LabelLayoutSinglePage": "Vue unique",
|
||||||
|
"LabelLayoutSplitPage": "Vue partagée",
|
||||||
"LabelLess": "Moins",
|
"LabelLess": "Moins",
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur",
|
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur",
|
||||||
"LabelLibrary": "Bibliothèque",
|
"LabelLibrary": "Bibliothèque",
|
||||||
"LabelLibraryItem": "Article de bibliothèque",
|
"LabelLibraryItem": "Article de bibliothèque",
|
||||||
"LabelLibraryName": "Nom de la bibliothèque",
|
"LabelLibraryName": "Nom de la bibliothèque",
|
||||||
"LabelLimit": "Limite",
|
"LabelLimit": "Limite",
|
||||||
|
"LabelLineSpacing": "Interligne",
|
||||||
"LabelListenAgain": "Écouter à nouveau",
|
"LabelListenAgain": "Écouter à nouveau",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
@@ -305,16 +321,17 @@
|
|||||||
"LabelNewPassword": "Nouveau mot de passe",
|
"LabelNewPassword": "Nouveau mot de passe",
|
||||||
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
|
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
|
||||||
"LabelNextScheduledRun": "Prochain lancement prévu",
|
"LabelNextScheduledRun": "Prochain lancement prévu",
|
||||||
|
"LabelNoEpisodesSelected": "Aucun épisode sélectionné",
|
||||||
"LabelNotes": "Notes",
|
"LabelNotes": "Notes",
|
||||||
"LabelNotFinished": "Non terminé(e)",
|
"LabelNotFinished": "Non terminé(e)",
|
||||||
"LabelNotificationAppriseURL": "URL(s) d’apprise",
|
"LabelNotificationAppriseURL": "URL(s) d’Apprise",
|
||||||
"LabelNotificationAvailableVariables": "Variables disponibles",
|
"LabelNotificationAvailableVariables": "Variables disponibles",
|
||||||
"LabelNotificationBodyTemplate": "Modèle de Message",
|
"LabelNotificationBodyTemplate": "Modèle de Message",
|
||||||
"LabelNotificationEvent": "Evènement de Notification",
|
"LabelNotificationEvent": "Evènement de Notification",
|
||||||
"LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi",
|
"LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi",
|
||||||
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
||||||
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
|
"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",
|
"LabelNotificationTitleTemplate": "Modèle de Titre",
|
||||||
"LabelNotStarted": "Non Démarré(e)",
|
"LabelNotStarted": "Non Démarré(e)",
|
||||||
"LabelNumberOfBooks": "Nombre de Livres",
|
"LabelNumberOfBooks": "Nombre de Livres",
|
||||||
@@ -338,20 +355,23 @@
|
|||||||
"LabelPodcastType": "Type de Podcast",
|
"LabelPodcastType": "Type de Podcast",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"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",
|
"LabelProgress": "Progression",
|
||||||
"LabelProvider": "Fournisseur",
|
"LabelProvider": "Fournisseur",
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublisher": "Éditeur",
|
"LabelPublisher": "Éditeur",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"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",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de parution",
|
"LabelReleaseDate": "Date de parution",
|
||||||
"LabelRemoveCover": "Supprimer la couverture",
|
"LabelRemoveCover": "Supprimer la couverture",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||||
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||||
@@ -361,23 +381,32 @@
|
|||||||
"LabelSearchTitle": "Titre de recherche",
|
"LabelSearchTitle": "Titre de recherche",
|
||||||
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
||||||
"LabelSeason": "Saison",
|
"LabelSeason": "Saison",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
|
||||||
|
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
|
||||||
|
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
|
||||||
"LabelSequence": "Séquence",
|
"LabelSequence": "Séquence",
|
||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
"LabelSeriesProgress": "Progression de séries",
|
"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",
|
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de date",
|
"LabelSettingsDateFormat": "Format de date",
|
||||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
|
"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*",
|
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
||||||
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"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)",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
"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",
|
"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",
|
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
||||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
||||||
@@ -404,6 +433,7 @@
|
|||||||
"LabelShowAll": "Afficher Tout",
|
"LabelShowAll": "Afficher Tout",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
"LabelSleepTimer": "Minuterie",
|
"LabelSleepTimer": "Minuterie",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
"LabelStart": "Démarrer",
|
"LabelStart": "Démarrer",
|
||||||
"LabelStarted": "Démarré",
|
"LabelStarted": "Démarré",
|
||||||
"LabelStartedAt": "Démarré à",
|
"LabelStartedAt": "Démarré à",
|
||||||
@@ -428,8 +458,11 @@
|
|||||||
"LabelTag": "Étiquette",
|
"LabelTag": "Étiquette",
|
||||||
"LabelTags": "Étiquettes",
|
"LabelTags": "Étiquettes",
|
||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à l’utilisateur",
|
||||||
"LabelTasks": "Tâches en cours",
|
"LabelTasks": "Tâches en cours",
|
||||||
|
"LabelTheme": "Thème",
|
||||||
|
"LabelThemeDark": "Sombre",
|
||||||
|
"LabelThemeLight": "Clair",
|
||||||
"LabelTimeBase": "Base de temps",
|
"LabelTimeBase": "Base de temps",
|
||||||
"LabelTimeListened": "Temps d’écoute",
|
"LabelTimeListened": "Temps d’écoute",
|
||||||
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
||||||
@@ -448,6 +481,7 @@
|
|||||||
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
|
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
|
||||||
"LabelTracks": "Pistes",
|
"LabelTracks": "Pistes",
|
||||||
"LabelTracksMultiTrack": "Piste multiple",
|
"LabelTracksMultiTrack": "Piste multiple",
|
||||||
|
"LabelTracksNone": "No tracks",
|
||||||
"LabelTracksSingleTrack": "Piste simple",
|
"LabelTracksSingleTrack": "Piste simple",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Version intégrale",
|
"LabelUnabridged": "Version intégrale",
|
||||||
@@ -481,32 +515,36 @@
|
|||||||
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
||||||
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0}: {1} »",
|
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0}: {1} »",
|
||||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
||||||
"MessageBookshelfNoSeries": "Vous n’avez aucune séries",
|
"MessageBookshelfNoSeries": "Vous n’avez aucune série",
|
||||||
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
||||||
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
||||||
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
"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",
|
"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…",
|
"MessageCheckingCron": "Vérification du cron…",
|
||||||
|
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
"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} » ?",
|
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
"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 ?",
|
"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 ?",
|
"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 ?",
|
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
"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} » ?",
|
"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é.",
|
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||||
"MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà « {0} ».",
|
"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.",
|
"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} ».",
|
"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",
|
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||||
"MessageEmbedFinished": "Intégration Terminée !",
|
"MessageEmbedFinished": "Intégration Terminée !",
|
||||||
@@ -525,8 +563,10 @@
|
|||||||
"MessageM4BFailed": "M4B en échec !",
|
"MessageM4BFailed": "M4B en échec !",
|
||||||
"MessageM4BFinished": "M4B terminé !",
|
"MessageM4BFinished": "M4B terminé !",
|
||||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
"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é",
|
"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.",
|
"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",
|
"MessageNoAudioTracks": "Aucune piste audio",
|
||||||
"MessageNoAuthors": "Aucun auteur",
|
"MessageNoAuthors": "Aucun auteur",
|
||||||
@@ -565,7 +605,6 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n’a pas d’URL de flux RSS à utiliser pour la correspondance",
|
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n’a pas d’URL de flux RSS à utiliser pour la correspondance",
|
||||||
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de « {0} ». N’écrase pas les données présentes à moins que le paramètre « Préférer les Métadonnées par correspondance » soit activé.",
|
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de « {0} ». N’écrase pas les données présentes à moins que le paramètre « Préférer les Métadonnées par correspondance » soit activé.",
|
||||||
"MessageRemoveAllItemsWarning": "ATTENTION ! Cette action supprimera toute la base de données de la bibliothèque ainsi que les mises à jour ou correspondances qui auraient été effectuées. Cela n’a aucune incidence sur les fichiers de la bibliothèque. Souhaitez-vous continuer ?",
|
|
||||||
"MessageRemoveChapter": "Supprimer le chapitre",
|
"MessageRemoveChapter": "Supprimer le chapitre",
|
||||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d’écoute",
|
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d’écoute",
|
||||||
@@ -594,7 +633,7 @@
|
|||||||
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
@@ -661,8 +700,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
"ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
|
||||||
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user