Compare commits

...

138 Commits

Author SHA1 Message Date
advplyr c2793fe29b Fix:Crash when author is set without a name #1934 2023-07-19 17:13:57 -05:00
advplyr 24b9ac6a68 Version bump 2.3.3 2023-07-19 16:32:41 -05:00
advplyr 9a5ed64fae Update database loading library items incrementally to reduce mem usage 2023-07-19 15:36:18 -05:00
advplyr c2af96e7cd Fix:New user setting tags array #1933 2023-07-18 17:43:33 -05:00
advplyr 104cadb0b3 Version bump 2.3.2 2023-07-17 17:49:12 -05:00
advplyr 6814adffcc Update:Only load feeds when needed 2023-07-17 16:48:46 -05:00
advplyr 20c11e381e Update docker file heap size 2023-07-17 14:41:21 -05:00
advplyr b5952f16eb Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-07-17 13:58:25 -05:00
advplyr 5b6878e5de Fix:Crash on local playback sessions #1912 2023-07-17 13:58:19 -05:00
advplyr 89a25bcf39 Merge pull request #1917 from burghy86/patch-10
Update it.json
2023-07-17 08:09:28 -05:00
advplyr d0cd512be8 Fix:Crash when updating sequence on series #1919 2023-07-17 08:09:08 -05:00
advplyr 3543dea0fb Merge pull request #1923 from JBlond/master
Update German language file
2023-07-17 08:09:07 -05:00
advplyr 1949e25ccb Merge pull request #1925 from Machou/patch-1
Update fr.json
2023-07-17 08:08:43 -05:00
advplyr b715ef3bfc Increase heap size to 4gb in Dockerfile 2023-07-17 07:48:23 -05:00
Machou 954050df81 Update fr.json 2023-07-17 14:28:56 +02:00
JBlond e4aa7f10fa Update German language file 2023-07-17 10:02:54 +02:00
advplyr 2afd0e2acd Update dbMigration for old main library ids 2023-07-16 16:39:59 -05:00
advplyr 0829237166 Fix:Libraries out of order #1911 2023-07-16 15:43:46 -05:00
advplyr 541975f038 Version bump 2.3.1 2023-07-16 15:34:35 -05:00
advplyr 01bf58ab97 Fix createAuthor 2023-07-16 15:29:43 -05:00
advplyr d99b2c25e8 Fixes for db migration & local playback sessions 2023-07-16 15:05:51 -05:00
burghy86 a31df5ff81 Update it.json
traslate new string and fix error
2023-07-16 21:50:15 +02:00
advplyr 63e5cf2e60 Fix:Accessing series page for some users #787 2023-07-16 08:39:08 -05:00
advplyr 7beca048e7 Version bump v2.3.0 2023-07-15 15:29:25 -05:00
advplyr ec998dc1ac Update:Podcast library item covers show number of episodes incomplete #782 2023-07-15 14:45:08 -05:00
advplyr ddc54c8811 Update:Downloading library item shows log on the server with username #1461 2023-07-15 13:39:12 -05:00
advplyr 72e306935f Update:Support and as separator between multiple authors #1790 2023-07-15 13:28:31 -05:00
advplyr 96a7c7f4d1 Fix:Embedded chapters with invalid IDs, update chapter ids to always be the index #1783 2023-07-15 12:46:51 -05:00
advplyr 9c65d655b8 Fix:Realtime update cover on cover tab in item edit modal 2023-07-15 12:37:33 -05:00
advplyr b108f2241b Add:Library filter for publishers & link to publisher filter on book page #1813 2023-07-15 12:22:13 -05:00
advplyr 9439acf300 Merge pull request #1906 from warnwar/master
stop opf importer from adding duplicate info
2023-07-15 11:44:41 -05:00
advplyr d181e66d83 Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:44 -05:00
advplyr a87c3f2c77 Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:40 -05:00
advplyr 2834f6077e Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:35 -05:00
advplyr 918013ccb3 Add:Option on podcast page to mark all episodes as finished/unfinished #1862 2023-07-15 11:27:06 -05:00
advplyr 4c4672c6c1 Update:Item page UI for details that take up multiple lines 2023-07-15 11:00:07 -05:00
advplyr b3991574c7 Merge pull request #1907 from advplyr/sqlite_2
Migration to use sqlite3
2023-07-14 15:11:23 -05:00
advplyr c881bcbe59 Update logs for cache purge 2023-07-14 15:04:27 -05:00
advplyr 89aa4a8bdc Update logger to support dev only log, remove old model docs 2023-07-14 14:50:37 -05:00
advplyr c5a4f63670 Update Backup to use key to check for old backups no longer supported 2023-07-14 14:20:35 -05:00
advplyr 1b97582975 Update dbMigration mappings 2023-07-14 14:04:47 -05:00
advplyr 9b7aacf3ea Update dbMigration mappings 2023-07-14 14:04:28 -05:00
WarWar 47b9ee557e stop opf importer from adding duplicate info 2023-07-14 05:15:29 +00:00
advplyr e40e0bfa25 Update:Listening session modal UI 2023-07-13 17:44:20 -05:00
advplyr d56e3a3617 Merge branch 'master' into sqlite_2 2023-07-11 17:07:13 -05:00
advplyr 78fe6d47ba Fix:Library settings context menu actions for mobile view #1886 2023-07-11 17:06:14 -05:00
advplyr 995cf51ae3 Update:Default m4b encoding bitrate to 128k #1892 2023-07-11 16:57:30 -05:00
advplyr d838ff2f2e Merge branch 'master' into sqlite_2 2023-07-10 17:37:47 -05:00
advplyr f2f07ff534 Update:Show num episodes on podcast item page #1891 2023-07-10 17:37:35 -05:00
advplyr 8cff68ca64 Fix purge metadata/items paths 2023-07-10 17:00:31 -05:00
advplyr eb5331d34a Update playlist & collection models to use sort order 2023-07-10 16:07:22 -05:00
advplyr f425185575 Merge branch 'master' into sqlite_2 2023-07-09 15:50:50 -05:00
advplyr 9fc352a5a4 Fix:Download episode from rss feed with very long description #1893 2023-07-09 15:50:40 -05:00
advplyr e85ddc1aa1 Update package.json pkg assets, remove njodb and dependencies 2023-07-09 14:22:30 -05:00
advplyr b9be7510f8 Remove purge-media-progress api route 2023-07-09 14:08:14 -05:00
advplyr f4497acd48 Remove API routes for removing all items and purging media progress 2023-07-09 14:07:30 -05:00
advplyr f73a0cce72 Update Dockerfile for sqlite3, update models for cascade delete, fix backup schedule 2023-07-09 11:39:15 -05:00
advplyr 254ba1f089 Migrate backups manager 2023-07-08 14:40:49 -05:00
advplyr 0a179e4eed Update author and series to include libraryId 2023-07-08 10:07:57 -05:00
advplyr 0ac63b2678 Update Series and Author model to be library specific 2023-07-08 09:57:32 -05:00
advplyr 1d13d0a553 Merge master 2023-07-08 08:25:33 -05:00
advplyr fc6ff016a7 Update:Playback sync request timeout to 9s and show sync alerts after 4 failed syncs #1884 2023-07-08 08:08:14 -05:00
advplyr e378b79fbc Fix:Access series that are in multiple libraries and user does not have access to all #1899, new libraries/series endpoint 2023-07-07 17:59:17 -05:00
advplyr 7e377297d7 Update:Remove toast notifications for marking items as finished #1900 2023-07-07 17:22:38 -05:00
advplyr 00a02921dd Fix:RSS feeds that include an id as a query string #1896 2023-07-06 18:06:26 -05:00
advplyr b5d4c11f6f Fix RSS feeds to use slug instead of id 2023-07-06 17:07:10 -05:00
advplyr a0bc959850 Add feed migration and cleanup 2023-07-05 18:18:37 -05:00
advplyr a4b0f6c202 Merge branch 'master' into sqlite_2 2023-07-04 18:15:52 -05:00
advplyr 65cf928afe Fix:Delete ereader device 2023-07-04 18:15:43 -05:00
advplyr cf7fd315b6 Init sqlite take 2 2023-07-04 18:14:44 -05:00
advplyr d86a3b3dc2 Update:Filter out podcasts from search that dont have an RSS feed url #1514 2023-07-01 09:00:40 -05:00
advplyr e07e2cd359 Update:Select all episodes showing option #1878 & add translations to episodes modal 2023-06-30 17:30:15 -05:00
advplyr 8140d7021a Update:Increase timeout for progress sync to 6s and sync interval to 10s #1884 2023-06-30 16:28:02 -05:00
advplyr bdbc5e3161 Add:Library setting to hide single book series #1433 2023-06-29 17:55:17 -05:00
advplyr bb9013541b Update:Get all users api endpoint to include latest session, display device info on users table #724 2023-06-28 17:57:46 -05:00
advplyr 1668153acd Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-06-27 17:13:38 -05:00
advplyr aeba7674f8 Add new api route for downloading backup, remove static metadata route 2023-06-27 16:41:32 -05:00
advplyr 5b0d105e21 Remove deprecated /s/ and /ebook/ api routes 2023-06-27 15:56:33 -05:00
advplyr feb54d0629 Merge pull request #1874 from springsunx/master
update zh-cn.json
2023-06-27 08:53:23 -05:00
SunX 3284fe8f31 update zh-cn.json
update zh-cn.json
2023-06-27 21:23:20 +08:00
advplyr 18cb394884 Update:Remove episodes from newest shelf when finished #1871 2023-06-26 17:32:45 -05:00
advplyr d0bce2949e Add:FFProbe api endpoint 2023-06-25 16:16:11 -05:00
advplyr a0e80772cd Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-06-23 17:32:09 -05:00
advplyr e44595521d Update:Cleanup collections edit modal ui for mobile 2023-06-23 17:32:03 -05:00
advplyr fdf647eb32 Update:Cleanup chapters page ui on mobile 2023-06-23 17:28:35 -05:00
advplyr 71369bd2a0 Update:Podcast rss feed fetch timeout to 12s #1856 2023-06-22 17:27:09 -05:00
advplyr 36b1f43f4c Fix:epub ereader on mobile #1854 2023-06-18 14:10:01 -05:00
advplyr a8bc1df3e7 Fix epub ereader theme sticking for other ebook formats 2023-06-18 12:56:32 -05:00
advplyr a96869f547 Add ereader translations 2023-06-16 17:00:40 -05:00
advplyr 77b030199e Fix:Non-admin access to config pages #1848 and dev proxy #1848 2023-06-15 17:41:27 -05:00
advplyr 0e1c6c0ba7 Merge pull request #1849 from Nab0y/master
Update Russian localization
2023-06-15 16:10:24 -05:00
Dmitry Naboychenko c397422d3b Update russian localization 2023-06-15 23:28:14 +03:00
advplyr 15313826bf Add:Epub ereader settings for font scale, line spacing, theme and spread 2023-06-14 17:30:08 -05:00
advplyr c6405b9013 Merge pull request #1838 from daVinci2793/master
Updates to Email settings/manager to include test email
2023-06-12 17:16:18 -05:00
advplyr d748d43efc Fallback to using from address if test address is not set, add reset button when form has changes 2023-06-12 17:12:52 -05:00
daVinci2793 d54edb93d6 Updates to Email settings/manager to include test email 2023-06-12 04:53:51 +00:00
advplyr b8ca6671fc Minor cleanup 2023-06-11 13:22:58 -05:00
advplyr cb7fb646ba Fix:Comic reader next/prev buttons 2023-06-11 11:37:28 -05:00
advplyr aa82c8a253 Version bump 2.2.23 2023-06-10 16:00:48 -05:00
advplyr aae92649b1 Add:Ebook and supplementary ebook library filters 2023-06-10 15:59:44 -05:00
advplyr a9f5c64204 Update:Cleanup UI/UX for filter and sort dropdowns 2023-06-10 15:46:12 -05:00
advplyr 1392baf1eb Fix:Remove experimental features 2023-06-10 15:28:21 -05:00
advplyr 0ec50bb570 Remove experimental features and experimental ereader setting 2023-06-10 14:11:51 -05:00
advplyr b60473d7ae Update:Setting new other ebook files as supplementary #1809 2023-06-10 13:20:38 -05:00
advplyr 014fc45c15 Add:Audiobooks only library settings, supplementary ebooks #1664 2023-06-10 12:46:57 -05:00
advplyr 4b4fb33d8f Fix:Pressing edit on a podcast episode from a playlist #1833 2023-06-09 17:12:38 -05:00
advplyr 35e3458fb4 Add:Download button in comic reader to download current page image #1822 2023-06-07 17:03:23 -05:00
advplyr 8f42153bee Add:Save progress for comics #1829 2023-06-07 16:14:48 -05:00
advplyr 2f04d34bce Fix:Submenu overflowing page #1828 2023-06-07 15:48:23 -05:00
advplyr 09566c02ea Fix:Series page In Progress filter showing completed series #1827 2023-06-07 14:01:03 -05:00
advplyr d714ef37d9 Fix:Using arrow keys when editing podcast description #1826 2023-06-07 11:01:11 -05:00
advplyr fde07d26e5 Update:Prefer epub ebook file when setting ebook #1825, validate ebookLocation 2023-06-06 16:53:11 -05:00
advplyr 9547824aaa Merge pull request #1819 from mayli/usetemp
Fix: useTempFiles=true, upload use tmp instead of ram
2023-06-06 15:41:28 -05:00
advplyr 5a01be1ee3 Add tempFileDir for uploads 2023-06-06 15:40:52 -05:00
advplyr 5dc4606657 Add:Support for CAF audio files 2023-06-05 16:23:40 -05:00
Coda 2fd3238576 Fix: useTempFiles=true, upload use tmp instead of ram 2023-06-04 15:56:41 -07:00
advplyr c1bcfe8304 Merge pull request #1816 from mayli/utf8-filename
Fix: decode filename as utf8 on upload
2023-06-04 08:39:38 -05:00
Coda a3642b204d Fix: decode filename as utf8 on upload 2023-06-03 21:44:13 -07:00
advplyr 8243da69f6 Merge pull request #1814 from depwl9992/patch-1
Update readme.md - Expand required Apache modules for reverse proxy and detail how to handle acme challenges
2023-06-03 17:30:16 -05:00
Daniel Powell 6d5987b2e0 Update readme.md
After setting up the reverse proxy for Apache per the current instructions, I received the error: "AH01144: No protocol handler was valid for the URL / (scheme 'http')". Installing proxy_http and proxy_balancer modules in addition to those listed fixed this issue.

a2enmod command does not include the redundant '_module' suffix.

Let's Encrypt was not able to validate certificates either automatically or manually as ABS does not respond to ACME challenges directly. Editing the virtual environment configuration to ignore proxy requests to .well-known and serving that directly from Apache allowed LE to validate the challenge instead.
2023-06-03 11:46:42 -06:00
advplyr a2fdc3e876 Update:Increase max height of libraries dropdown 2023-06-01 17:09:04 -05:00
advplyr f92b66a469 Merge pull request #1810 from glacasa/master
Update french translations
2023-06-01 16:29:48 -05:00
Guillaume Lacasa c3d256c42b Update french translations 2023-06-01 09:38:00 +02:00
advplyr fdc792cb82 Version bump v2.2.22 2023-05-30 16:59:04 -05:00
advplyr a16fb31e6e Update:Library filter max height #1673 2023-05-30 16:55:52 -05:00
advplyr 4d8a1b5b6d Add:Ebook library filter, and update e-book to ebook 2023-05-30 16:37:24 -05:00
advplyr c382f07b05 Fix:Close player resetting progress #1807 2023-05-30 16:08:30 -05:00
advplyr 9f6a7d065c Merge pull request #1808 from Lionfox2/patch-1
Update de.json
2023-05-30 04:14:33 -05:00
Lionfox2 11aa75ecbe Update de.json
I corrected a spelling mistake (starseite --> startseite)
2023-05-30 00:56:27 +02:00
advplyr 05ce9c6eda Add:Email smtp config & send ebooks to devices #1474 2023-05-29 17:38:38 -05:00
advplyr 15aaf2863c Add:OPML Export #1260 2023-05-28 15:10:34 -05:00
advplyr 019063e6f4 Update:New API routes for library files and downloads 2023-05-28 12:34:22 -05:00
advplyr ea79948122 Fix:Podcast episode downloads where RSS feed uses the same title #1802 2023-05-28 11:24:51 -05:00
advplyr 7a0f27e3cc Fix:Epub3 background color #1804 2023-05-28 10:55:37 -05:00
advplyr 4f75a89633 Update:New EBook API endpoint 2023-05-28 10:47:28 -05:00
advplyr b3f19ef628 Fix:Static file route check authorization 2023-05-28 09:34:03 -05:00
advplyr f16e312319 Fix:Series api check user has access to library 2023-05-28 08:51:34 -05:00
advplyr 056da0ef70 Fix:Static ebook route 2023-05-28 08:39:41 -05:00
236 changed files with 12224 additions and 6333 deletions
+9 -1
View File
@@ -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,6 +27,10 @@ COPY server server
RUN npm ci --only=production RUN npm ci --only=production
RUN apk del make python3 g++
ENV NODE_OPTIONS=--max-old-space-size=4096
EXPOSE 80 EXPOSE 80
HEALTHCHECK \ HEALTHCHECK \
--interval=30s \ --interval=30s \
+4 -7
View File
@@ -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')
}, },
@@ -211,7 +208,7 @@ export default {
} }
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
}, },
contextMenuAction(action) { contextMenuAction({ action }) {
if (action === 'quick-embed') { if (action === 'quick-embed') {
this.requestBatchQuickEmbed() this.requestBatchQuickEmbed()
} else if (action === 'quick-match') { } else if (action === 'quick-match') {
@@ -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,9 +65,6 @@ 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
}, },
@@ -171,7 +168,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
}) })
+26 -1
View File
@@ -81,6 +81,8 @@
<!-- 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="110" class="ml-2" @action="contextMenuAction" />
</template> </template>
<!-- search page --> <!-- search page -->
<template v-else-if="page === 'search'"> <template v-else-if="page === 'search'">
@@ -186,6 +188,9 @@ export default {
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']
}, },
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
@@ -276,10 +281,30 @@ export default {
}, },
isIssuesFilter() { isIssuesFilter() {
return this.filterBy === 'issues' && this.$route.query.filter === 'issues' return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
},
contextMenuItems() {
const items = []
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
items.push({
text: 'Export OPML',
action: 'export-opml'
})
}
return items
} }
}, },
methods: { methods: {
seriesContextMenuAction(action) { contextMenuAction({ action }) {
if (action === 'export-opml') {
this.exportOPML()
}
},
exportOPML() {
this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true)
},
seriesContextMenuAction({ action }) {
if (action === 'open-rss-feed') { if (action === 'open-rss-feed') {
this.showOpenSeriesRSSFeed() this.showOpenSeriesRSSFeed()
} else if (action === 're-add-to-continue-listening') { } else if (action === 're-add-to-continue-listening') {
+5
View File
@@ -90,6 +90,11 @@ export default {
title: this.$strings.HeaderNotifications, title: this.$strings.HeaderNotifications,
path: '/config/notifications' path: '/config/notifications'
}, },
{
id: 'config-email',
title: this.$strings.HeaderEmail,
path: '/config/email'
},
{ {
id: 'config-item-metadata-utils', id: 'config-item-metadata-utils',
title: this.$strings.HeaderItemMetadataUtils, title: this.$strings.HeaderItemMetadataUtils,
+2 -5
View File
@@ -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']
}, },
@@ -318,10 +315,10 @@ export default {
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&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
}) })
+60 -16
View File
@@ -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 || {}
}, },
@@ -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
}, },
@@ -367,13 +368,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
@@ -448,7 +449,6 @@ export default {
} }
] ]
if (this.continueListeningShelf) { if (this.continueListeningShelf) {
items.push({ items.push({
func: 'removeFromContinueListening', func: 'removeFromContinueListening',
text: this.$strings.ButtonRemoveFromContinueListening text: this.$strings.ButtonRemoveFromContinueListening
@@ -490,6 +490,18 @@ export default {
text: this.$strings.LabelAddToPlaylist text: this.$strings.LabelAddToPlaylist
}) })
} }
if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) {
items.push({
text: this.$strings.LabelSendEbookToDevice,
subitems: this.store.state.libraries.ereaderDevices.map((d) => {
return {
text: d.name,
func: 'sendToDevice',
data: d.name
}
})
})
}
} }
if (this.userCanUpdate) { if (this.userCanUpdate) {
items.push({ items.push({
@@ -675,7 +687,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)
@@ -720,7 +731,40 @@ export default {
// More menu func // More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' }) this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
}, },
sendToDevice(deviceName) {
// More menu func
const payload = {
// message: `Are you sure you want to send ${this.ebookFormat} ebook "${this.title}" to device "${deviceName}"?`,
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFormat, this.title, deviceName]),
callback: (confirmed) => {
if (confirmed) {
const payload = {
libraryItemId: this.libraryItemId,
deviceName
}
this.processing = true
const axios = this.$axios || this.$nuxt.$axios
axios
.$post(`/api/emails/send-ebook-to-device`, payload)
.then(() => {
this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
})
.catch((error) => {
console.error('Failed to send ebook to device', error)
this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
})
.finally(() => {
this.processing = false
})
}
},
type: 'yesNo'
}
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
@@ -833,8 +877,8 @@ export default {
items: this.moreMenuItems items: this.moreMenuItems
}, },
created() { created() {
this.$on('action', (func) => { this.$on('action', (action) => {
if (_this[func]) _this[func]() if (action.func && _this[action.func]) _this[action.func](action.data)
}) })
this.$on('close', () => { this.$on('close', () => {
_this.isMoreMenuOpen = false _this.isMoreMenuOpen = false
@@ -846,7 +890,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
@@ -879,7 +923,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
@@ -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>
+9 -4
View File
@@ -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>
+2 -2
View File
@@ -197,8 +197,8 @@ export default {
} }
</script> </script>
<style> <style scoped>
.globalSearchMenu { .globalSearchMenu {
max-height: 80vh; max-height: calc(100vh - 75px);
} }
</style> </style>
@@ -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,31 +9,35 @@
<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>
<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 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 md:text-base">{{ 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',
@@ -185,6 +197,11 @@ export default {
value: 'tracks', value: 'tracks',
sublist: true sublist: true
}, },
{
text: this.$strings.LabelEbooks,
value: 'ebooks',
sublist: true
},
{ {
text: this.$strings.LabelAbridged, text: this.$strings.LabelAbridged,
value: 'abridged', value: 'abridged',
@@ -255,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
} }
@@ -302,6 +323,9 @@ export default {
languages() { languages() {
return this.filterData.languages || [] return this.filterData.languages || []
}, },
publishers() {
return this.filterData.publishers || []
},
progress() { progress() {
return [ return [
{ {
@@ -334,6 +358,18 @@ export default {
} }
] ]
}, },
ebooks() {
return [
{
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
}
]
},
missing() { missing() {
return [ return [
{ {
@@ -391,7 +427,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,
@@ -404,6 +440,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 || {}
@@ -428,7 +471,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
@@ -439,4 +482,10 @@ export default {
} }
} }
} }
</script> </script>
<style scoped>
.libraryFilterMenu {
max-height: calc(100vh - 125px);
}
</style>
@@ -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>
+4 -4
View File
@@ -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>
+3 -3
View File
@@ -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: []
} }
} }
} }
+132 -76
View File
@@ -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>
+1 -1
View File
@@ -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>
+3 -3
View File
@@ -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>
@@ -0,0 +1,171 @@
<template>
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12">
<div class="flex items-center -mx-1 mb-2">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
</div>
</div>
<div class="flex items-center pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
existingDevices: {
type: Array,
default: () => []
},
ereaderDevice: {
type: Object,
default: () => null
}
},
data() {
return {
processing: false,
newDevice: {
name: '',
email: ''
}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return this.ereaderDevice ? 'Create Device' : 'Update Device'
}
},
methods: {
submitForm() {
this.$refs.ereaderNameInput.blur()
this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error('Name and email required')
return
}
this.newDevice.name = this.newDevice.name.trim()
this.newDevice.email = this.newDevice.email.trim()
if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists')
return
}
this.submitCreate()
} else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists')
return
}
this.submitUpdate()
}
},
submitUpdate() {
this.processing = true
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
const payload = {
ereaderDevices: [
...existingDevicesWithoutThisOne,
{
...this.newDevice
}
]
}
this.$axios
.$post(`/api/emails/ereader-devices`, payload)
.then((data) => {
this.$emit('update', data.ereaderDevices)
this.$toast.success('Device updated')
this.show = false
})
.catch((error) => {
console.error('Failed to update device', error)
this.$toast.error('Failed to update device')
})
.finally(() => {
this.processing = false
})
},
submitCreate() {
this.processing = true
const payload = {
ereaderDevices: [
...this.existingDevices,
{
...this.newDevice
}
]
}
this.$axios
.$post('/api/emails/ereader-devices', payload)
.then((data) => {
this.$emit('update', data.ereaderDevices || [])
this.$toast.success('Device added')
this.show = false
})
.catch((error) => {
console.error('Failed to add device', error)
this.$toast.error('Failed to add device')
})
.finally(() => {
this.processing = false
})
},
init() {
if (this.ereaderDevice) {
this.newDevice.name = this.ereaderDevice.name
this.newDevice.email = this.ereaderDevice.email
} else {
this.newDevice.name = ''
this.newDevice.email = ''
}
}
},
mounted() {}
}
</script>
@@ -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
+14 -11
View File
@@ -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">
@@ -36,10 +36,10 @@
</div> </div>
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2"> <div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
<template v-for="cover in localCovers"> <template v-for="localCoverFile in localCovers">
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)"> <div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }"> <div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
<covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="localCoverFile.localPath" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
</div> </div>
</template> </template>
@@ -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']
@@ -169,8 +172,8 @@ export default {
return this.libraryFiles return this.libraryFiles
.filter((f) => f.fileType === 'image') .filter((f) => f.fileType === 'image')
.map((file) => { .map((file) => {
var _file = { ...file } const _file = { ...file }
_file.localPath = `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}` _file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
return _file return _file
}) })
} }
+2 -5
View File
@@ -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">
@@ -17,18 +17,38 @@
</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>
@@ -47,7 +67,9 @@ export default {
useSquareBookCovers: false, useSquareBookCovers: false,
disableWatcher: false, disableWatcher: 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
@@ -72,7 +97,9 @@ export default {
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.disableWatcher,
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin, skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries
} }
} }
}, },
@@ -84,6 +111,8 @@ export default {
this.disableWatcher = !!this.librarySettings.disableWatcher this.disableWatcher = !!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))
+111 -21
View File
@@ -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>
@@ -57,12 +60,13 @@ Archive.init({
export default { export default {
props: { props: {
url: String,
libraryItem: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
playerOpen: Boolean playerOpen: Boolean,
keepProgress: Boolean,
fileId: String
}, },
data() { data() {
return { return {
@@ -72,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,
@@ -88,17 +93,79 @@ export default {
} }
}, },
computed: { computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
ebookUrl() {
if (this.fileId) {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
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
@@ -111,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() {
@@ -146,10 +216,11 @@ export default {
}, },
async extract() { async extract() {
this.loading = true this.loading = true
console.log('Extracting', this.url) var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob',
var buff = await this.$axios.$get(this.url, { headers: {
responseType: 'blob' Authorization: `Bearer ${this.userToken}`
}
}) })
const archive = await Archive.open(buff) const archive = await Archive.open(buff)
const originalFilesObject = await archive.getFilesObject() const originalFilesObject = await archive.getFilesObject()
@@ -165,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')
+89 -29
View File
@@ -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>
@@ -24,12 +24,13 @@ import ePub from 'epubjs'
*/ */
export default { export default {
props: { props: {
url: String,
libraryItem: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
playerOpen: Boolean playerOpen: Boolean,
keepProgress: Boolean,
fileId: String
}, },
data() { data() {
return { return {
@@ -38,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: {
@@ -47,6 +54,9 @@ export default {
} }
}, },
computed: { computed: {
userToken() {
return this.$store.getters['user/getToken']
},
/** @returns {string} */ /** @returns {string} */
libraryItemId() { libraryItemId() {
return this.libraryItem?.id return this.libraryItem?.id
@@ -59,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}`
}, },
@@ -75,9 +92,46 @@ export default {
readerHeight() { readerHeight() {
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
},
ebookUrl() {
if (this.fileId) {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `/api/items/${this.libraryItemId}/ebook`
},
themeRules() {
const isDark = this.ereaderSettings.theme === 'dark'
const fontColor = isDark ? '#fff' : '#000'
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)'
const lineSpacing = this.ereaderSettings.lineSpacing / 100
const fontScale = this.ereaderSettings.fontScale / 100
return {
'*': {
color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`,
'line-height': lineSpacing * fontScale + 'rem!important'
},
a: {
color: `${fontColor}!important`
}
}
} }
}, },
methods: { 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() {
return this.rendition?.prev() return this.rendition?.prev()
}, },
@@ -101,6 +155,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)
}) })
@@ -192,7 +247,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
} }
@@ -212,43 +267,42 @@ export default {
const reader = this const reader = this
/** @type {ePub.Book} */ /** @type {ePub.Book} */
reader.book = new ePub(reader.url, { reader.book = new ePub(reader.ebookUrl, {
width: this.readerWidth, width: this.readerWidth,
height: this.readerHeight - 50 height: this.readerHeight - 50,
openAs: 'epub',
requestHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}) })
/** @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' } }) 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
@@ -266,6 +320,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() {
+21 -5
View File
@@ -15,17 +15,30 @@ import defaultCss from '@/assets/ebooks/basic.js'
export default { export default {
props: { props: {
url: String,
libraryItem: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
playerOpen: Boolean playerOpen: Boolean,
fileId: String
}, },
data() { data() {
return {} return {}
}, },
computed: {}, computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
ebookUrl() {
if (this.fileId) {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `/api/items/${this.libraryItemId}/ebook`
}
},
methods: { methods: {
addHtmlCss() { addHtmlCss() {
let iframe = document.getElementsByTagName('iframe')[0] let iframe = document.getElementsByTagName('iframe')[0]
@@ -83,8 +96,11 @@ export default {
}, },
async initMobi() { async initMobi() {
// Fetch mobi file as blob // Fetch mobi file as blob
var buff = await this.$axios.$get(this.url, { var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob' responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
}) })
var reader = new FileReader() var reader = new FileReader()
reader.onload = async (event) => { reader.onload = async (event) => {
+30 -7
View File
@@ -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>
@@ -23,7 +23,7 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto"> <div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div> <div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf> <pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
</div> </div>
</div> </div>
</div> </div>
@@ -41,12 +41,13 @@ export default {
pdf pdf
}, },
props: { props: {
url: String,
libraryItem: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
playerOpen: Boolean playerOpen: Boolean,
keepProgress: Boolean,
fileId: String
}, },
data() { data() {
return { return {
@@ -60,6 +61,9 @@ export default {
} }
}, },
computed: { computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() { libraryItemId() {
return this.libraryItem?.id return this.libraryItem?.id
}, },
@@ -93,7 +97,25 @@ 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() {
return {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
methods: { methods: {
@@ -104,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
@@ -118,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
} }
}, },
+193 -34
View File
@@ -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" :url="ebookUrl" :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() {
@@ -128,31 +221,36 @@ export default {
isComic() { isComic() {
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr' return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
}, },
ebookUrl() {
if (!this.ebookFile) return null
let filepath = ''
if (this.selectedLibraryItem.isFile) {
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
} else {
const itemRelPath = this.selectedLibraryItem.relPath
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
const relPath = this.ebookFile.metadata.relPath
if (relPath.startsWith('/')) relPath = relPath.slice(1)
filepath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
}
return `/ebook/${this.libraryId}/${this.folderId}/${filepath}`
},
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
@@ -170,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()
-1
View File
@@ -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
@@ -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>
@@ -73,11 +73,11 @@ export default {
return items return items
}, },
downloadUrl() { downloadUrl() {
return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.track.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}` return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}/download?token=${this.userToken}`
} }
}, },
methods: { methods: {
contextMenuAction(action) { contextMenuAction({ action }) {
if (action === 'delete') { if (action === 'delete') {
this.deleteLibraryFile() this.deleteLibraryFile()
} else if (action === 'download') { } else if (action === 'download') {
@@ -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
@@ -107,15 +107,7 @@ export default {
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
}, },
downloadLibraryFile() { downloadLibraryFile() {
const a = document.createElement('a') this.$downloadFile(this.downloadUrl, this.track.metadata.filename)
a.style.display = 'none'
a.href = this.downloadUrl
a.download = this.track.metadata.filename
document.body.appendChild(a)
a.click()
setTimeout(() => {
a.remove()
})
} }
}, },
mounted() {} mounted() {}
+10 -6
View File
@@ -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>
@@ -45,7 +45,7 @@ export default {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
downloadUrl() { downloadUrl() {
return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.file.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}` return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.file.ino}/download?token=${this.userToken}`
}, },
contextMenuItems() { contextMenuItems() {
const items = [] const items = []
@@ -72,7 +72,7 @@ export default {
} }
}, },
methods: { methods: {
contextMenuAction(action) { contextMenuAction({ action }) {
if (action === 'delete') { if (action === 'delete') {
this.deleteLibraryFile() this.deleteLibraryFile()
} else if (action === 'download') { } else if (action === 'download') {
@@ -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
@@ -102,15 +102,7 @@ export default {
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
}, },
downloadLibraryFile() { downloadLibraryFile() {
const a = document.createElement('a') this.$downloadFile(this.downloadUrl, this.file.metadata.filename)
a.style.display = 'none'
a.href = this.downloadUrl
a.download = this.file.metadata.filename
document.body.appendChild(a)
a.click()
setTimeout(() => {
a.remove()
})
} }
}, },
mounted() {} mounted() {}
@@ -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)
+1 -1
View File
@@ -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>
+15 -4
View File
@@ -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)
@@ -94,7 +94,7 @@ export default {
} }
}, },
methods: { methods: {
contextMenuAction(action) { contextMenuAction({ action }) {
this.showMobileMenu = false this.showMobileMenu = false
if (action === 'edit') { if (action === 'edit') {
this.editClick() this.editClick()
@@ -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() {
@@ -185,19 +201,36 @@ export default {
this.searchText = this.search.toLowerCase().trim() this.searchText = this.search.toLowerCase().trim()
}, 500) }, 500)
}, },
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)
+1 -1
View File
@@ -73,7 +73,7 @@ export default {
} }
</script> </script>
<style> <style scoped>
.btn::before { .btn::before {
content: ''; content: '';
position: absolute; position: absolute;
+66 -12
View File
@@ -1,16 +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 max-h-56 rounded-md py-1 overflow-auto 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">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)"> <template v-if="item.subitems">
<p>{{ item.text }}</p> <div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
<p>{{ item.text }}</p>
</div>
<div
v-if="mouseoverItemIndex === index"
:key="`subitems-${index}`"
@mouseover="mouseoverSubItemMenu(index)"
@mouseleave="mouseleaveSubItemMenu(index)"
class="absolute bg-bg border rounded-b-md border-black-200 shadow-lg z-50 -ml-px py-1"
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
>
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
<p>{{ subitem.text }}</p>
</div>
</div>
</template>
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
<p class="text-left">{{ item.text }}</p>
</div> </div>
</template> </template>
</div> </div>
@@ -31,9 +52,10 @@ export default {
default: '' default: ''
}, },
menuWidth: { menuWidth: {
type: String, type: Number,
default: '192px' default: 192
} },
processing: Boolean
}, },
data() { data() {
return { return {
@@ -42,22 +64,54 @@ export default {
events: ['mousedown'], events: ['mousedown'],
isActive: true isActive: true
}, },
showMenu: false submenuWidth: 144,
showMenu: false,
mouseoverItemIndex: null,
isOverSubItemMenu: false,
openSubMenuLeft: false
}
},
computed: {
submenuLeftPos() {
return this.openSubMenuLeft ? -(this.submenuWidth - 1) : this.menuWidth - 0.5
} }
}, },
computed: {},
methods: { methods: {
mouseoverSubItemMenu(index) {
this.isOverSubItemMenu = true
},
mouseleaveSubItemMenu(index) {
setTimeout(() => {
if (this.isOverSubItemMenu && this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
}, 1)
},
mouseoverItem(index) {
this.isOverSubItemMenu = false
this.mouseoverItemIndex = index
},
mouseleaveItem(index) {
setTimeout(() => {
if (this.isOverSubItemMenu) return
if (this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
}, 1)
},
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
}, },
clickAction(action) { clickAction(action, data) {
if (this.disabled) return if (this.disabled) return
this.showMenu = false this.showMenu = false
this.$emit('action', action) this.$emit('action', { action, data })
} }
}, },
mounted() {} mounted() {}
+17 -3
View File
@@ -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>
-61
View File
@@ -1,61 +0,0 @@
<template>
<div class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
<span class="flex items-center">
<span class="block truncate">{{ label }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-2xl text-gray-100" aria-label="User Account" role="button">person</span>
</span>
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
<template v-for="item in items">
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
</div>
</li>
</nuxt-link>
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
</div>
</li>
</template>
</ul>
</transition>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: 'Menu'
},
items: {
type: Array,
default: () => []
}
},
data() {
return {
showMenu: false
}
},
methods: {
clickOutside() {
this.showMenu = false
},
clickedOption(itemValue) {
this.$emit('action', itemValue)
this.showMenu = false
}
},
mounted() {}
}
</script>
+86
View File
@@ -0,0 +1,86 @@
<template>
<div class="inline-flex">
<input v-model="input" type="range" :min="min" :max="max" :step="step" />
<p class="text-sm ml-2">{{ input }}%</p>
</div>
</template>
<script>
export default {
props: {
value: [String, Number],
min: Number,
max: Number,
step: Number
},
data() {
return {}
},
computed: {
input: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {},
mounted() {}
}
</script>
<style scoped>
input[type='range'] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type='range']:focus {
outline: none;
}
/* chromium */
input[type='range']::-webkit-slider-runnable-track {
background-color: rgb(0 0 0 / 0.25);
border-radius: 9999px;
height: 0.75rem;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -0.25rem;
border-radius: 9999px;
background-color: rgb(255 255 255 / 0.7);
height: 1.25rem;
width: 1.25rem;
}
input[type='range']:focus::-webkit-slider-thumb {
border: 1px solid #6b6b6b;
outline: 3px solid #6b6b6b;
outline-offset: 0.125rem;
}
/* firefox */
input[type='range']::-moz-range-track {
background-color: rgb(0 0 0 / 0.25);
border-radius: 9999px;
height: 0.75rem;
}
input[type='range']::-moz-range-thumb {
border: none;
border-radius: 9999px;
margin-top: -0.25rem;
background-color: rgb(255 255 255 / 0.7);
height: 1.25rem;
width: 1.25rem;
}
input[type='range']:focus::-moz-range-thumb {
border: 1px solid #6b6b6b;
outline: 3px solid #6b6b6b;
outline-offset: 0.125rem;
}
</style>
+2 -1
View File
@@ -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
+85
View File
@@ -0,0 +1,85 @@
<template>
<div class="inline-flex toggle-btn-wrapper shadow-md">
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-none relative border border-gray-600 px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
{{ item.text }}
</button>
</div>
</template>
<script>
export default {
props: {
value: String,
/**
* [{ "text", "", "value": "" }]
*/
items: {
type: Array,
default: Object
}
},
data() {
return {}
},
computed: {},
methods: {
clickBtn(value) {
this.$emit('input', value)
}
},
mounted() {}
}
</script>
<style scoped>
.toggle-btn-wrapper .toggle-btn:first-child {
border-top-left-radius: 0.375rem /* 6px */;
border-bottom-left-radius: 0.375rem /* 6px */;
}
.toggle-btn-wrapper .toggle-btn:last-child {
border-top-right-radius: 0.375rem /* 6px */;
border-bottom-right-radius: 0.375rem /* 6px */;
}
.toggle-btn-wrapper .toggle-btn:first-child::before {
border-top-left-radius: 0.375rem /* 6px */;
border-bottom-left-radius: 0.375rem /* 6px */;
}
.toggle-btn-wrapper .toggle-btn:last-child::before {
border-top-right-radius: 0.375rem /* 6px */;
border-bottom-right-radius: 0.375rem /* 6px */;
}
.toggle-btn-wrapper .toggle-btn:not(:first-child) {
margin-left: -1px;
}
.toggle-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.toggle-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
.toggle-btn:hover:not(:disabled) {
color: white;
}
.toggle-btn {
color: rgba(255, 255, 255, 0.75);
}
.toggle-btn.selected {
color: white;
}
.toggle-btn.selected::before {
background-color: rgba(255, 255, 255, 0.1);
}
button.toggle-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>
+3 -1
View File
@@ -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
+54 -7
View File
@@ -1,7 +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">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.func)"> <template v-if="item.subitems">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
<p>{{ item.text }}</p>
</div>
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute bg-bg rounded-b-md border border-black-200 py-1 shadow-lg z-50" :class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'" :style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }">
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.func, subitem.data)">
<p>{{ subitem.text }}</p>
</div>
</div>
</template>
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/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>
@@ -22,13 +32,43 @@ export default {
handler: this.clickedOutside, handler: this.clickedOutside,
events: ['mousedown'], events: ['mousedown'],
isActive: true isActive: true
} },
submenuWidth: 144,
menuWidth: 144,
mouseoverItemIndex: null,
isOverSubItemMenu: false,
openSubMenuLeft: false
}
},
computed: {
submenuLeftPos() {
return this.openSubMenuLeft ? -this.submenuWidth : this.menuWidth - 1.5
} }
}, },
computed: {},
methods: { methods: {
clickAction(func) { mouseoverSubItemMenu(index) {
this.$emit('action', func) this.isOverSubItemMenu = true
},
mouseleaveSubItemMenu(index) {
setTimeout(() => {
if (this.isOverSubItemMenu && this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
}, 1)
},
mouseoverItem(index) {
this.isOverSubItemMenu = false
this.mouseoverItemIndex = index
},
mouseleaveItem(index) {
setTimeout(() => {
if (this.isOverSubItemMenu) return
if (this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
}, 1)
},
clickAction(func, data) {
this.$emit('action', {
func,
data
})
this.close() this.close()
}, },
clickedOutside(e) { clickedOutside(e) {
@@ -44,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>
+11 -9
View File
@@ -380,6 +380,11 @@ export default {
adminMessageEvt(message) { adminMessageEvt(message) {
this.$toast.info(message) this.$toast.info(message)
}, },
ereaderDevicesUpdated(data) {
if (!data?.ereaderDevices) return
this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
},
initializeSocket() { initializeSocket() {
this.socket = this.$nuxtSocket({ this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod', name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
@@ -452,6 +457,9 @@ export default {
this.socket.on('task_finished', this.taskFinished) this.socket.on('task_finished', this.taskFinished)
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate) this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
// EReader Device Listeners
this.socket.on('ereader-devices-updated', this.ereaderDevicesUpdated)
this.socket.on('backup_applied', this.backupApplied) this.socket.on('backup_applied', this.backupApplied)
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete) this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
@@ -483,9 +491,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
@@ -552,12 +560,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')
+2 -3
View File
@@ -71,9 +71,8 @@ module.exports = {
], ],
proxy: { proxy: {
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }, '/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }, '/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' + process.env : '/' },
}, },
io: { io: {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.21", "version": "2.3.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.21", "version": "2.3.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.21", "version": "2.3.3",
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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'
} }
+1 -1
View File
@@ -145,7 +145,7 @@ export default {
feed: this.rssFeed feed: this.rssFeed
}) })
}, },
contextMenuAction(action) { contextMenuAction({ action }) {
if (action === 'delete') { if (action === 'delete') {
this.removeClick() this.removeClick()
} else if (action === 'create-playlist') { } else if (action === 'create-playlist') {
+2 -12
View File
@@ -4,7 +4,7 @@
<div class="configContent" :class="`page-${currentPage}`"> <div class="configContent" :class="`page-${currentPage}`">
<div v-show="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore"> <div v-show="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore">
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span> <span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
<p class="pl-3 capitalize">{{ $strings.HeaderSettings }}</p> <p class="pl-3 capitalize">{{ currentPage }}</p>
</div> </div>
<nuxt-child /> <nuxt-child />
</div> </div>
@@ -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 === 'email') return this.$strings.HeaderEmail
} }
return this.$strings.HeaderSettings return this.$strings.HeaderSettings
} }
@@ -79,14 +80,6 @@ export default {
width: 900px; width: 900px;
max-width: calc(100% - 176px); max-width: calc(100% - 176px);
} }
.configContent.page-library-stats {
width: 1200px;
}
@media (max-width: 1550px) {
.configContent.page-library-stats {
margin-left: 176px;
}
}
@media (max-width: 1240px) { @media (max-width: 1240px) {
.configContent { .configContent {
margin-left: 176px; margin-left: 176px;
@@ -98,8 +91,5 @@ export default {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
.configContent.page-library-stats {
margin-left: 0px;
}
} }
</style> </style>
+21 -12
View File
@@ -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 } : {}
+260
View File
@@ -0,0 +1,260 @@
<template>
<div>
<app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
<form @submit.prevent="submitForm">
<div class="flex items-center -mx-1 mb-2">
<div class="w-full md:w-3/4 px-1">
<ui-text-input-with-label ref="hostInput" v-model="newSettings.host" :disabled="savingSettings" :label="$strings.LabelHost" />
</div>
<div class="w-full md:w-1/4 px-1">
<ui-text-input-with-label ref="portInput" v-model="newSettings.port" type="number" :disabled="savingSettings" :label="$strings.LabelPort" />
</div>
</div>
<div class="flex items-center mb-2 py-3">
<ui-toggle-switch labeledBy="email-settings-secure" v-model="newSettings.secure" :disabled="savingSettings" />
<ui-tooltip :text="$strings.LabelEmailSettingsSecureHelp">
<div class="pl-4 flex items-center">
<span id="email-settings-secure">{{ $strings.LabelEmailSettingsSecure }}</span>
<span class="material-icons text-lg pl-1">info_outlined</span>
</div>
</ui-tooltip>
</div>
<div class="flex items-center -mx-1 mb-2">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="userInput" v-model="newSettings.user" :disabled="savingSettings" :label="$strings.LabelUsername" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="passInput" v-model="newSettings.pass" type="password" :disabled="savingSettings" :label="$strings.LabelPassword" />
</div>
</div>
<div class="flex items-center -mx-1 mb-2">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="fromInput" v-model="newSettings.fromAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsFromAddress" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="testInput" v-model="newSettings.testAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsTestAddress" />
</div>
</div>
<div class="flex items-center justify-between pt-4">
<ui-btn v-if="hasUpdates" :disabled="savingSettings" type="button" @click="resetChanges">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn v-else :loading="sendingTest" :disabled="savingSettings || !newSettings.host" type="button" @click="sendTestClick">{{ $strings.ButtonTest }}</ui-btn>
<ui-btn :loading="savingSettings" :disabled="!hasUpdates" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</form>
<div v-show="loading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-25 flex items-center justify-center">
<ui-loading-indicator />
</div>
</app-settings-content>
<app-settings-content :header-text="$strings.HeaderEreaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick">
<table v-if="existingEReaderDevices.length" class="tracksTable my-4">
<tr>
<th class="text-left">{{ $strings.LabelName }}</th>
<th class="text-left">{{ $strings.LabelEmail }}</th>
<th class="w-40"></th>
</tr>
<tr v-for="device in existingEReaderDevices" :key="device.name">
<td>
<p class="text-sm md:text-base text-gray-100">{{ device.name }}</p>
</td>
<td class="text-left">
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
</td>
<td class="w-40">
<div class="flex justify-end items-center h-10">
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" class="mx-1" @click="editDeviceClick(device)" />
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" @click="deleteDeviceClick(device)" />
</div>
</td>
</tr>
</table>
<div v-else class="text-center py-4">
<p class="text-lg text-gray-100">No Devices</p>
</div>
</app-settings-content>
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
</div>
</template>
<script>
export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
}
},
data() {
return {
loading: false,
savingSettings: false,
sendingTest: false,
deletingDeviceName: null,
settings: null,
newSettings: {
host: null,
port: 465,
secure: true,
user: null,
pass: null,
testAddress: null,
fromAddress: null
},
newEReaderDevice: {
name: '',
email: ''
},
selectedEReaderDevice: null,
showEReaderDeviceModal: false
}
},
computed: {
hasUpdates() {
if (!this.settings) return true
for (const key in this.newSettings) {
if (key === 'ereaderDevices') continue
if (this.newSettings[key] !== this.settings[key]) return true
}
return false
},
existingEReaderDevices() {
return this.settings?.ereaderDevices || []
}
},
methods: {
resetChanges() {
this.newSettings = {
...this.settings
}
},
editDeviceClick(device) {
this.selectedEReaderDevice = device
this.showEReaderDeviceModal = true
},
deleteDeviceClick(device) {
const payload = {
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
callback: (confirmed) => {
if (confirmed) {
this.deleteDevice(device)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteDevice(device) {
const payload = {
ereaderDevices: this.existingEReaderDevices.filter((d) => d.name !== device.name)
}
this.deletingDeviceName = device.name
this.$axios
.$post(`/api/emails/ereader-devices`, payload)
.then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices)
this.$toast.success('Device deleted')
})
.catch((error) => {
console.error('Failed to delete device', error)
this.$toast.error('Failed to delete device')
})
.finally(() => {
this.deletingDeviceName = null
})
},
ereaderDevicesUpdated(ereaderDevices) {
this.settings.ereaderDevices = ereaderDevices
this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
},
addNewDeviceClick() {
this.selectedEReaderDevice = null
this.showEReaderDeviceModal = true
},
sendTestClick() {
this.sendingTest = true
this.$axios
.$post('/api/emails/test')
.then(() => {
this.$toast.success('Test Email Sent')
})
.catch((error) => {
console.error('Failed to send test email', error)
const errorMsg = error.response.data || 'Failed to send test email'
this.$toast.error(errorMsg)
})
.finally(() => {
this.sendingTest = false
})
},
validateForm() {
for (const ref of [this.$refs.hostInput, this.$refs.portInput, this.$refs.userInput, this.$refs.passInput, this.$refs.fromInput]) {
if (ref?.blur) ref.blur()
}
if (this.newSettings.port) {
this.newSettings.port = Number(this.newSettings.port)
}
return true
},
submitForm() {
if (!this.validateForm()) return
const updatePayload = {
host: this.newSettings.host,
port: this.newSettings.port,
secure: this.newSettings.secure,
user: this.newSettings.user,
pass: this.newSettings.pass,
testAddress: this.newSettings.testAddress,
fromAddress: this.newSettings.fromAddress
}
this.savingSettings = true
this.$axios
.$patch('/api/emails/settings', updatePayload)
.then((data) => {
this.settings = data.settings
this.newSettings = {
...data.settings
}
this.$toast.success('Email settings updated')
})
.catch((error) => {
console.error('Failed to update email settings', error)
this.$toast.error('Failed to update email settings')
})
.finally(() => {
this.savingSettings = false
})
},
init() {
this.loading = true
this.$axios
.$get(`/api/emails/settings`)
.then((data) => {
this.settings = data.settings
this.newSettings = {
...this.settings
}
})
.catch((error) => {
console.error('Failed to get email settings', error)
this.$toast.error('Failed to load email settings')
})
.finally(() => {
this.loading = false
})
}
},
mounted() {
this.init()
},
beforeDestroy() {}
}
</script>
+7 -47
View File
@@ -166,7 +166,8 @@
</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 +181,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 +192,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,6 +248,11 @@
<script> <script>
export default { export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
}
},
data() { data() {
return { return {
isResettingLibraryItems: false, isResettingLibraryItems: false,
@@ -303,14 +288,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
}, },
@@ -390,23 +367,6 @@ export default {
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,
+5
View File
@@ -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,
+5
View File
@@ -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')
} }
+5
View File
@@ -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,
+5
View File
@@ -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,
+6 -1
View File
@@ -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) => {
+4 -2
View File
@@ -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() {
+1 -29
View File
@@ -47,12 +47,6 @@
<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">
<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"> <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>
@@ -111,8 +105,7 @@ export default {
data() { data() {
return { return {
listeningSessions: {}, listeningSessions: {},
listeningStats: {}, listeningStats: {}
purgingMediaProgress: false
} }
}, },
computed: { computed: {
@@ -134,9 +127,6 @@ export default {
mediaProgressWithMedia() { mediaProgressWithMedia() {
return this.mediaProgress.filter((mp) => mp.media) 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 +166,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() {
+5
View File
@@ -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,
+54 -30
View File
@@ -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>
@@ -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 || ''
}, },
@@ -431,6 +420,19 @@ export default {
}) })
} }
if (this.ebookFile && this.$store.state.libraries.ereaderDevices?.length) {
items.push({
text: this.$strings.LabelSendEbookToDevice,
subitems: this.$store.state.libraries.ereaderDevices.map((d) => {
return {
text: d.name,
action: 'sendToDevice',
data: d.name
}
})
})
}
if (this.userCanDelete) { if (this.userCanDelete) {
items.push({ items.push({
text: this.$strings.ButtonDelete, text: this.$strings.ButtonDelete,
@@ -506,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) {
@@ -531,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)
@@ -677,14 +678,7 @@ export default {
} }
}, },
downloadLibraryItem() { downloadLibraryItem() {
const a = document.createElement('a') this.$downloadFile(this.downloadUrl)
a.style.display = 'none'
a.href = this.downloadUrl
document.body.appendChild(a)
a.click()
setTimeout(() => {
a.remove()
})
}, },
deleteLibraryItem() { deleteLibraryItem() {
const payload = { const payload = {
@@ -711,7 +705,35 @@ export default {
} }
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
}, },
contextMenuAction(action) { sendToDevice(deviceName) {
const payload = {
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFile.ebookFormat, this.title, deviceName]),
callback: (confirmed) => {
if (confirmed) {
const payload = {
libraryItemId: this.libraryItemId,
deviceName
}
this.processing = true
this.$axios
.$post(`/api/emails/send-ebook-to-device`, payload)
.then(() => {
this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
})
.catch((error) => {
console.error('Failed to send ebook to device', error)
this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
})
.finally(() => {
this.processing = false
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
contextMenuAction({ action, data }) {
if (action === 'collections') { if (action === 'collections') {
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setShowCollectionsModal', true) this.$store.commit('globals/setShowCollectionsModal', true)
@@ -726,6 +748,8 @@ export default {
this.downloadLibraryItem() this.downloadLibraryItem()
} else if (action === 'delete') { } else if (action === 'delete') {
this.deleteLibraryItem() this.deleteLibraryItem()
} else if (action === 'sendToDevice') {
this.sendToDevice(data)
} }
} }
}, },
@@ -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
+1 -1
View File
@@ -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
}) })
+17 -8
View File
@@ -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}`)
} }
} }
@@ -107,7 +115,7 @@ export default {
const payload = { const payload = {
newRoot: { ...this.newRoot } newRoot: { ...this.newRoot }
} }
var success = await this.$axios const success = await this.$axios
.$post('/init', payload) .$post('/init', payload)
.then(() => true) .then(() => true)
.catch((error) => { .catch((error) => {
@@ -124,9 +132,10 @@ export default {
location.reload() location.reload()
}, },
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) { setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source) this.$store.commit('setSource', Source)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
this.$setServerLanguageCode(serverSettings.language) this.$setServerLanguageCode(serverSettings.language)
if (serverSettings.chromecastEnabled) { if (serverSettings.chromecastEnabled) {
@@ -143,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)
@@ -161,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
+10 -4
View File
@@ -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
} }
+1 -1
View File
@@ -7,7 +7,7 @@ export default function ({ $axios, store, $config }) {
if (config.url.startsWith('http:') || config.url.startsWith('https:')) { if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return return
} }
var bearerToken = store.state.user.user ? store.state.user.user.token : null const bearerToken = store.state.user.user?.token || null
if (bearerToken) { if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}` config.headers.common['Authorization'] = `Bearer ${bearerToken}`
} }
+1 -1
View File
@@ -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'],
+1 -1
View File
@@ -34,7 +34,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]
+11 -8
View File
@@ -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)
} }
}) })
} }
+19
View File
@@ -145,6 +145,25 @@ Vue.prototype.$getNextScheduledDate = (expression) => {
return interval.next().toDate() return interval.next().toDate()
} }
Vue.prototype.$downloadFile = (url, filename = null, openInNewTab = false) => {
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
if (filename) {
a.download = filename
}
if (openInNewTab) {
a.target = '_blank'
}
document.body.appendChild(a)
a.click()
setTimeout(() => {
a.remove()
})
}
export function supplant(str, subs) { export function supplant(str, subs) {
// source: http://crockford.com/javascript/remedial.html // source: http://crockford.com/javascript/remedial.html
return str.replace(/{([^{}]*)}/g, return str.replace(/{([^{}]*)}/g,
+7 -7
View File
@@ -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
+5 -6
View File
@@ -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
}, },
+29 -16
View File
@@ -11,7 +11,8 @@ export const state = () => ({
filterData: null, filterData: null,
numUserPlaylists: 0, numUserPlaylists: 0,
collections: [], collections: [],
userPlaylists: [] userPlaylists: [],
ereaderDevices: []
}) })
export const getters = { export const getters = {
@@ -56,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)
}, },
@@ -234,21 +238,23 @@ export const mutations = {
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 {
@@ -259,9 +265,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 {
@@ -272,7 +278,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)
@@ -282,7 +288,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)
@@ -292,7 +298,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)
@@ -301,12 +307,16 @@ export const mutations = {
}) })
} }
// Add publishers
if (mediaMetadata.publisher && !state.filterData.publishers.includes(mediaMetadata.publisher)) {
state.filterData.publishers.push(mediaMetadata.publisher)
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
}
// Add language // 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) {
@@ -339,5 +349,8 @@ export const mutations = {
removeUserPlaylist(state, playlist) { removeUserPlaylist(state, playlist) {
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id) state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
state.numUserPlaylists = state.userPlaylists.length state.numUserPlaylists = state.userPlaylists.length
},
setEReaderDevices(state, ereaderDevices) {
state.ereaderDevices = ereaderDevices
} }
} }
+8 -8
View File
@@ -19,7 +19,7 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root', getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'), getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => { getToken: (state) => {
return state.user ? state.user.token : null return state.user?.token || null
}, },
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => { getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
if (!state.user.mediaProgress) return null if (!state.user.mediaProgress) return null
@@ -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'
+58 -13
View File
@@ -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",
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B-Kodierung starten", "ButtonStartM4BEncode": "M4B-Kodierung starten",
"ButtonStartMetadataEmbed": "Metadateneinbettung starten", "ButtonStartMetadataEmbed": "Metadateneinbettung starten",
"ButtonSubmit": "Ok", "ButtonSubmit": "Ok",
"ButtonTest": "Test",
"ButtonUpload": "Hochladen", "ButtonUpload": "Hochladen",
"ButtonUploadBackup": "Sicherung hochladen", "ButtonUploadBackup": "Sicherung hochladen",
"ButtonUploadCover": "Titelbild hochladen", "ButtonUploadCover": "Titelbild hochladen",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Aktuelle Downloads", "HeaderCurrentDownloads": "Aktuelle Downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange", "HeaderDownloadQueue": "Download Warteschlange",
"HeaderEbookFiles": "E-Book Dateien",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Einstellungen",
"HeaderEpisodes": "Episoden", "HeaderEpisodes": "Episoden",
"HeaderEreaderDevices": "Ereader Geräte",
"HeaderEreaderSettings": "Ereader Einstellungen",
"HeaderFiles": "Dateien", "HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen", "HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien", "HeaderIgnoredFiles": "Ignorierte Dateien",
@@ -149,6 +155,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",
@@ -188,7 +195,7 @@
"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",
@@ -198,7 +205,7 @@
"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",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromFilename": "CD aus dem Dateinamen",
"LabelDiscFromMetadata": "CD aus den Metadaten", "LabelDiscFromMetadata": "CD aus den Metadaten",
"LabelDownload": "Herunterladen", "LabelDownload": "Herunterladen",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Laufzeit", "LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:", "LabelDurationFound": "Gefundene Laufzeit:",
"LabelEbook": "E-Book",
"LabelEbooks": "E-Books",
"LabelEdit": "Bearbeiten", "LabelEdit": "Bearbeiten",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Von Address",
"LabelEmailSettingsSecure": "Sicherheit",
"LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Addresse",
"LabelEmbeddedCover": "Eingebettetes Cover", "LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEnd": "Ende", "LabelEnd": "Ende",
@@ -229,7 +244,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",
@@ -237,10 +252,14 @@
"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",
"LabelHour": "Stunde", "LabelHour": "Stunde",
"LabelIcon": "Symbol", "LabelIcon": "Symbol",
"LabelIncludeInTracklist": "In die Titelliste aufnehmen", "LabelIncludeInTracklist": "In die Titelliste aufnehmen",
@@ -256,7 +275,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",
@@ -265,12 +284,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",
@@ -285,7 +308,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",
@@ -295,6 +318,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)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Typ", "LabelPodcastType": "Podcast Typ",
"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",
@@ -350,23 +378,30 @@
"LabelSearchTitle": "Titel", "LabelSearchTitle": "Titel",
"LabelSearchTitleOrASIN": "Titel oder ASIN", "LabelSearchTitleOrASIN": "Titel oder ASIN",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"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",
"LabelSettingsEnableEReaderHelp": "Der E-Reader befindet sich noch in der Entwicklung, aber mit dieser Einstellung können Sie ihn für alle Benutzer aktivieren (oder aktivieren Sie die Option \"Experimentelle Funktionen\", dann Sie ihn nur selbst verwenden)",
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen", "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",
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht", "LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher",
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel", "LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet", "LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
@@ -418,6 +453,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",
@@ -477,9 +515,12 @@
"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...",
"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": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", "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?",
@@ -494,6 +535,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": "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!",
@@ -512,6 +554,8 @@
"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": "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.",
@@ -552,7 +596,6 @@
"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": "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",
@@ -648,6 +691,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": "E-Book konnte nicht auf Gerät übertragen werden",
"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",
@@ -657,4 +702,4 @@
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
"ToastUserDeleteSuccess": "Benutzer gelöscht" "ToastUserDeleteSuccess": "Benutzer gelöscht"
} }
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Start M4B Encode", "ButtonStartM4BEncode": "Start M4B Encode",
"ButtonStartMetadataEmbed": "Start Metadata Embed", "ButtonStartMetadataEmbed": "Start Metadata Embed",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonTest": "Test",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup", "ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover", "ButtonUploadCover": "Upload Cover",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes", "HeaderEpisodes": "Episodes",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Files", "HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters", "HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files", "HeaderIgnoredFiles": "Ignored Files",
@@ -149,6 +155,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",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelDurationFound": "Duration found:", "LabelDurationFound": "Duration found:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Edit", "LabelEdit": "Edit",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"LabelEnd": "End", "LabelEnd": "End",
@@ -237,10 +252,14 @@
"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",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
@@ -265,12 +284,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",
@@ -295,6 +318,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)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"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",
@@ -350,22 +378,29 @@
"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...",
"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",
"LabelSettingsEnableEReaderHelp": "E-reader is still a work in progress, but use this setting to open it up to all your users (or use the \"Experimental Features\" toggle just for use by you)",
"LabelSettingsExperimentalFeatures": "Experimental features", "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",
@@ -418,6 +453,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",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
"MessageCheckingCron": "Checking cron...", "MessageCheckingCron": "Checking cron...",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!", "MessageEmbedFinished": "Embed Finished!",
@@ -512,6 +554,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.",
@@ -552,7 +596,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",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection", "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed", "ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Iniciar Codificación M4B", "ButtonStartM4BEncode": "Iniciar Codificación M4B",
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata", "ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Enviar",
"ButtonTest": "Test",
"ButtonUpload": "Subir", "ButtonUpload": "Subir",
"ButtonUploadBackup": "Subir Respaldo", "ButtonUploadBackup": "Subir Respaldo",
"ButtonUploadCover": "Subir Portada", "ButtonUploadCover": "Subir Portada",
@@ -97,7 +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",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodios", "HeaderEpisodes": "Episodios",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Elemento", "HeaderFiles": "Elemento",
"HeaderFindChapters": "Buscar Capitulo", "HeaderFindChapters": "Buscar Capitulo",
"HeaderIgnoredFiles": "Ignorar Elemento", "HeaderIgnoredFiles": "Ignorar Elemento",
@@ -149,6 +155,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",
@@ -216,9 +223,17 @@
"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",
"LabelDownload": "Descargar", "LabelDownload": "Descargar",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duración", "LabelDuration": "Duración",
"LabelDurationFound": "Duración Comprobada:", "LabelDurationFound": "Duración Comprobada:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Editar", "LabelEdit": "Editar",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Portada Integrada", "LabelEmbeddedCover": "Portada Integrada",
"LabelEnable": "Habilitar", "LabelEnable": "Habilitar",
"LabelEnd": "Fin", "LabelEnd": "Fin",
@@ -237,10 +252,14 @@
"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",
"LabelHour": "Hora", "LabelHour": "Hora",
"LabelIcon": "Icono", "LabelIcon": "Icono",
"LabelIncludeInTracklist": "Incluir en Tracklist", "LabelIncludeInTracklist": "Incluir en Tracklist",
@@ -265,12 +284,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",
@@ -295,6 +318,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)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Tipo Podcast", "LabelPodcastType": "Tipo Podcast",
"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",
@@ -350,22 +378,29 @@
"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...",
"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",
"LabelSettingsEnableEReaderHelp": "E-reader sigue en proceso, pero use esta configuración para hacerlo disponible a todos los usuarios. (o use las \"Funciones Experimentales\" para habilitarla para solo este usuario)",
"LabelSettingsExperimentalFeatures": "Funciones Experimentales", "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",
@@ -418,6 +453,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",
@@ -477,9 +515,12 @@
"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...",
"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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?", "MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.", "MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.",
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".", "MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Descargando Capitulo", "MessageDownloadingEpisode": "Descargando Capitulo",
"MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.", "MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.",
"MessageEmbedFinished": "Incorporación Terminada!", "MessageEmbedFinished": "Incorporación Terminada!",
@@ -512,6 +554,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.",
@@ -552,7 +596,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",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.", "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS", "ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada", "ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Error al actualizar la serie", "ToastSeriesUpdateFailed": "Error al actualizar la serie",
"ToastSeriesUpdateSuccess": "Series actualizada", "ToastSeriesUpdateSuccess": "Series actualizada",
"ToastSessionDeleteFailed": "Error al eliminar sesión", "ToastSessionDeleteFailed": "Error al eliminar sesión",
+72 -27
View File
@@ -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",
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Démarrer lencodage M4B", "ButtonStartM4BEncode": "Démarrer lencodage M4B",
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées", "ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
"ButtonSubmit": "Soumettre", "ButtonSubmit": "Soumettre",
"ButtonTest": "Test",
"ButtonUpload": "Téléverser", "ButtonUpload": "Téléverser",
"ButtonUploadBackup": "Téléverser une sauvegarde", "ButtonUploadBackup": "Téléverser une sauvegarde",
"ButtonUploadCover": "Téléverser une couverture", "ButtonUploadCover": "Téléverser une couverture",
@@ -86,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",
@@ -94,10 +95,15 @@
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la Collection", "HeaderCollectionItems": "Entrées de la Collection",
"HeaderCover": "Couverture", "HeaderCover": "Couverture",
"HeaderCurrentDownloads": "File dattente de téléchargement", "HeaderCurrentDownloads": "Téléchargements en cours",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
"HeaderDownloadQueue": "Queue de téléchargement", "HeaderDownloadQueue": "File dattente de téléchargements",
"HeaderEbookFiles": "Fichier des livres numériques",
"HeaderEmail": "Courriels",
"HeaderEmailSettings": "Configuration des courriels",
"HeaderEpisodes": "Épisodes", "HeaderEpisodes": "Épisodes",
"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",
@@ -149,6 +155,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 of Contents",
"HeaderTools": "Outils", "HeaderTools": "Outils",
"HeaderUpdateAccount": "Mettre à jour le compte", "HeaderUpdateAccount": "Mettre à jour le compte",
"HeaderUpdateAuthor": "Mettre à jour lauteur", "HeaderUpdateAuthor": "Mettre à jour lauteur",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Disque depuis le fichier", "LabelDiscFromFilename": "Disque depuis le fichier",
"LabelDiscFromMetadata": "Disque depuis les métadonnées", "LabelDiscFromMetadata": "Disque depuis les métadonnées",
"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": "Livre numérique",
"LabelEbooks": "Livres numériques",
"LabelEdit": "Modifier", "LabelEdit": "Modifier",
"LabelEmail": "Courriel",
"LabelEmailSettingsFromAddress": "Expéditeur",
"LabelEmailSettingsSecure": "Sécurisé",
"LabelEmailSettingsSecureHelp": "Si coché, la connexion utilisera TLS lors de la connexion au serveur. Sinon TLS est utilisé si le serveur prend en charge lextension 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",
@@ -227,9 +242,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 lutilisateur", "LabelFilterByUser": "Filtrer par lutilisateur",
@@ -237,16 +252,20 @@
"LabelFinished": "Fini(e)", "LabelFinished": "Fini(e)",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFolders": "Dossiers", "LabelFolders": "Dossiers",
"LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHardDeleteFile": "Suppression du fichier", "LabelHardDeleteFile": "Suppression du fichier",
"LabelHasEbook": "Dispose dun livre numérique",
"LabelHasSupplementaryEbook": "Dispose dun 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",
@@ -256,7 +275,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",
@@ -265,12 +284,16 @@
"LabelLastSeen": "Vu dernièrement", "LabelLastSeen": "Vu dernièrement",
"LabelLastTime": "Progression", "LabelLastTime": "Progression",
"LabelLastUpdate": "Dernière mise à jour", "LabelLastUpdate": "Dernière mise à jour",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Single page",
"LabelLayoutSplitPage": "Split page",
"LabelLess": "Moins", "LabelLess": "Moins",
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à lutilisateur", "LabelLibrariesAccessibleToUser": "Bibliothèque accessible à lutilisateur",
"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": "Line spacing",
"LabelListenAgain": "Écouter à nouveau", "LabelListenAgain": "Écouter à nouveau",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
@@ -295,16 +318,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": "No episodes selected",
"LabelNotes": "Notes", "LabelNotes": "Notes",
"LabelNotFinished": "Non terminé(e)", "LabelNotFinished": "Non terminé(e)",
"LabelNotificationAppriseURL": "URL(s) dapprise", "LabelNotificationAppriseURL": "URL(s) dApprise",
"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 denvoi", "LabelNotificationsMaxFailedAttempts": "Nombres de tentatives denvoi",
"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 dattente 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 dattente 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",
@@ -326,21 +350,25 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Type de Podcast", "LabelPodcastType": "Type de Podcast",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
"LabelPreventIndexing": "Empêcher lindexation de votre flux par les bases de donénes iTunes et Google podcast", "LabelPreventIndexing": "Empêcher lindexation 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 lindexation", "LabelRSSFeedPreventIndexing": "Empêcher lindexation",
@@ -350,22 +378,29 @@
"LabelSearchTitle": "Titre de recherche", "LabelSearchTitle": "Titre de recherche",
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN", "LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
"LabelSeason": "Saison", "LabelSeason": "Saison",
"LabelSelectAllEpisodes": "Select all episodes",
"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": "Lactivation de ce paramètre ignorera les fichiers “ ebook ”, à moins quils 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",
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre lactive pour tous les utilisateurs (ou utiliser linterrupteur « Fonctionnalités expérimentales » pour lactiver seulement pour vous)",
"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, lanalyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps danalyse.", "LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, lanalyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps danalyse.",
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent quun seul livre seront masquées sur la page de la série et sur les étagères de la page daccueil.",
"LabelSettingsHomePageBookshelfView": "La page daccueil utilise la vue étagère", "LabelSettingsHomePageBookshelfView": "La page daccueil 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",
@@ -418,6 +453,9 @@
"LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur", "LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tâches en cours", "LabelTasks": "Tâches en cours",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelTimeBase": "Base de temps", "LabelTimeBase": "Base de temps",
"LabelTimeListened": "Temps d’écoute", "LabelTimeListened": "Temps d’écoute",
"LabelTimeListenedToday": "Nombres d’écoutes Aujourdhui", "LabelTimeListenedToday": "Nombres d’écoutes Aujourdhui",
@@ -469,31 +507,35 @@
"MessageBookshelfNoCollections": "Vous navez pas encore de collections", "MessageBookshelfNoCollections": "Vous navez 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 nest ouvert", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",
"MessageBookshelfNoSeries": "Vous navez aucune séries", "MessageBookshelfNoSeries": "Vous navez 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…",
"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": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} \"{1}\" à lappareil \"{2}\"?",
"MessageDownloadingEpisode": "Téléchargement de l’épisode", "MessageDownloadingEpisode": "Téléchargement de l’épisode",
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans lordre correct", "MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans lordre correct",
"MessageEmbedFinished": "Intégration Terminée !", "MessageEmbedFinished": "Intégration Terminée !",
@@ -512,8 +554,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 lhorodatage.", "MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster lhorodatage.",
"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",
@@ -552,7 +596,6 @@
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection", "MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast na pas dURL de flux RSS à utiliser pour la correspondance", "MessagePodcastHasNoRSSFeedForMatching": "Le Podcast na pas dURL 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 na 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",
@@ -581,7 +624,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",
@@ -648,6 +691,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": "Échec de lenvoi du livre numérique à lappareil",
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à lappareil : {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",
@@ -657,4 +702,4 @@
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket", "ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
"ToastUserDeleteFailed": "Échec de la suppression de lutilisateur", "ToastUserDeleteFailed": "Échec de la suppression de lutilisateur",
"ToastUserDeleteSuccess": "Utilisateur supprimé" "ToastUserDeleteSuccess": "Utilisateur supprimé"
} }
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો", "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો", "ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
"ButtonSubmit": "સબમિટ કરો", "ButtonSubmit": "સબમિટ કરો",
"ButtonTest": "Test",
"ButtonUpload": "અપલોડ કરો", "ButtonUpload": "અપલોડ કરો",
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો", "ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
"ButtonUploadCover": "કવર અપલોડ કરો", "ButtonUploadCover": "કવર અપલોડ કરો",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes", "HeaderEpisodes": "Episodes",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Files", "HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters", "HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files", "HeaderIgnoredFiles": "Ignored Files",
@@ -149,6 +155,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",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelDurationFound": "Duration found:", "LabelDurationFound": "Duration found:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Edit", "LabelEdit": "Edit",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"LabelEnd": "End", "LabelEnd": "End",
@@ -237,10 +252,14 @@
"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",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
@@ -265,12 +284,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",
@@ -295,6 +318,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)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"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",
@@ -350,22 +378,29 @@
"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...",
"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",
"LabelSettingsEnableEReaderHelp": "E-reader is still a work in progress, but use this setting to open it up to all your users (or use the \"Experimental Features\" toggle just for use by you)",
"LabelSettingsExperimentalFeatures": "Experimental features", "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",
@@ -418,6 +453,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",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
"MessageCheckingCron": "Checking cron...", "MessageCheckingCron": "Checking cron...",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!", "MessageEmbedFinished": "Embed Finished!",
@@ -512,6 +554,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.",
@@ -552,7 +596,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",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection", "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed", "ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें", "ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
"ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें", "ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें",
"ButtonSubmit": "जमा करें", "ButtonSubmit": "जमा करें",
"ButtonTest": "Test",
"ButtonUpload": "अपलोड करें", "ButtonUpload": "अपलोड करें",
"ButtonUploadBackup": "बैकअप अपलोड करें", "ButtonUploadBackup": "बैकअप अपलोड करें",
"ButtonUploadCover": "कवर अपलोड करें", "ButtonUploadCover": "कवर अपलोड करें",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes", "HeaderEpisodes": "Episodes",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Files", "HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters", "HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files", "HeaderIgnoredFiles": "Ignored Files",
@@ -149,6 +155,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",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelDurationFound": "Duration found:", "LabelDurationFound": "Duration found:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Edit", "LabelEdit": "Edit",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"LabelEnd": "End", "LabelEnd": "End",
@@ -237,10 +252,14 @@
"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",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
@@ -265,12 +284,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",
@@ -295,6 +318,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)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"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",
@@ -350,22 +378,29 @@
"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...",
"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",
"LabelSettingsEnableEReaderHelp": "E-reader is still a work in progress, but use this setting to open it up to all your users (or use the \"Experimental Features\" toggle just for use by you)",
"LabelSettingsExperimentalFeatures": "Experimental features", "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",
@@ -418,6 +453,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",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
"MessageCheckingCron": "Checking cron...", "MessageCheckingCron": "Checking cron...",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!", "MessageEmbedFinished": "Embed Finished!",
@@ -512,6 +554,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.",
@@ -552,7 +596,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",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection", "ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed", "ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Pokreni M4B kodiranje", "ButtonStartM4BEncode": "Pokreni M4B kodiranje",
"ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka", "ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonTest": "Test",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload backup", "ButtonUploadBackup": "Upload backup",
"ButtonUploadCover": "Upload Cover", "ButtonUploadCover": "Upload Cover",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Detalji", "HeaderDetails": "Detalji",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Epizode", "HeaderEpisodes": "Epizode",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Datoteke", "HeaderFiles": "Datoteke",
"HeaderFindChapters": "Pronađi poglavlja", "HeaderFindChapters": "Pronađi poglavlja",
"HeaderIgnoredFiles": "Zanemarene datoteke", "HeaderIgnoredFiles": "Zanemarene datoteke",
@@ -149,6 +155,7 @@
"HeaderStatsRecentSessions": "Nedavne sesije", "HeaderStatsRecentSessions": "Nedavne sesije",
"HeaderStatsTop10Authors": "Top 10 autora", "HeaderStatsTop10Authors": "Top 10 autora",
"HeaderStatsTop5Genres": "Top 5 žanrova", "HeaderStatsTop5Genres": "Top 5 žanrova",
"HeaderTableOfContents": "Table of Contents",
"HeaderTools": "Alati", "HeaderTools": "Alati",
"HeaderUpdateAccount": "Aktualiziraj Korisnički račun", "HeaderUpdateAccount": "Aktualiziraj Korisnički račun",
"HeaderUpdateAuthor": "Aktualiziraj autora", "HeaderUpdateAuthor": "Aktualiziraj autora",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "CD iz imena datoteke", "LabelDiscFromFilename": "CD iz imena datoteke",
"LabelDiscFromMetadata": "CD iz metapodataka", "LabelDiscFromMetadata": "CD iz metapodataka",
"LabelDownload": "Preuzmi", "LabelDownload": "Preuzmi",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Trajanje", "LabelDuration": "Trajanje",
"LabelDurationFound": "Pronađeno trajanje:", "LabelDurationFound": "Pronađeno trajanje:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Uredi", "LabelEdit": "Uredi",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Uključi", "LabelEnable": "Uključi",
"LabelEnd": "Kraj", "LabelEnd": "Kraj",
@@ -237,10 +252,14 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folderi", "LabelFolders": "Folderi",
"LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Žanrovi", "LabelGenres": "Žanrovi",
"LabelHardDeleteFile": "Obriši datoteku zauvijek", "LabelHardDeleteFile": "Obriši datoteku zauvijek",
"LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHost": "Host",
"LabelHour": "Sat", "LabelHour": "Sat",
"LabelIcon": "Ikona", "LabelIcon": "Ikona",
"LabelIncludeInTracklist": "Dodaj u Tracklist", "LabelIncludeInTracklist": "Dodaj u Tracklist",
@@ -265,12 +284,16 @@
"LabelLastSeen": "Zadnje pogledano", "LabelLastSeen": "Zadnje pogledano",
"LabelLastTime": "Prošli put", "LabelLastTime": "Prošli put",
"LabelLastUpdate": "Zadnja aktualizacija", "LabelLastUpdate": "Zadnja aktualizacija",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Single page",
"LabelLayoutSplitPage": "Split page",
"LabelLess": "Manje", "LabelLess": "Manje",
"LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku", "LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku",
"LabelLibrary": "Biblioteka", "LabelLibrary": "Biblioteka",
"LabelLibraryItem": "Stavka biblioteke", "LabelLibraryItem": "Stavka biblioteke",
"LabelLibraryName": "Ime biblioteke", "LabelLibraryName": "Ime biblioteke",
"LabelLimit": "Limit", "LabelLimit": "Limit",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Slušaj ponovno", "LabelListenAgain": "Slušaj ponovno",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
@@ -295,6 +318,7 @@
"LabelNewPassword": "Nova lozinka", "LabelNewPassword": "Nova lozinka",
"LabelNextBackupDate": "Next backup date", "LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run", "LabelNextScheduledRun": "Next scheduled run",
"LabelNoEpisodesSelected": "No episodes selected",
"LabelNotes": "Bilješke", "LabelNotes": "Bilješke",
"LabelNotFinished": "Nedovršeno", "LabelNotFinished": "Nedovršeno",
"LabelNotificationAppriseURL": "Apprise URL(s)", "LabelNotificationAppriseURL": "Apprise URL(s)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)", "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
"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": "Napredak", "LabelProgress": "Napredak",
"LabelProvider": "Dobavljač", "LabelProvider": "Dobavljač",
"LabelPubDate": "Datam izdavanja", "LabelPubDate": "Datam izdavanja",
"LabelPublisher": "Izdavač", "LabelPublisher": "Izdavač",
"LabelPublishYear": "Godina izdavanja", "LabelPublishYear": "Godina izdavanja",
"LabelRead": "Read",
"LabelReadAgain": "Read Again", "LabelReadAgain": "Read Again",
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
"LabelRecentlyAdded": "Nedavno dodano", "LabelRecentlyAdded": "Nedavno dodano",
"LabelRecentSeries": "Nedavne serije", "LabelRecentSeries": "Nedavne serije",
"LabelRecommended": "Recommended", "LabelRecommended": "Recommended",
@@ -350,22 +378,29 @@
"LabelSearchTitle": "Traži naslov", "LabelSearchTitle": "Traži naslov",
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN", "LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sekvenca", "LabelSequence": "Sekvenca",
"LabelSeries": "Serije", "LabelSeries": "Serije",
"LabelSeriesName": "Ime serije", "LabelSeriesName": "Ime serije",
"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": "Skeumorfski (što god to bilo) dizajn sa drvenim policama", "LabelSettingsBookshelfViewHelp": "Skeumorfski (što god to bilo) dizajn sa drvenim policama",
"LabelSettingsChromecastSupport": "Chromecast podrška", "LabelSettingsChromecastSupport": "Chromecast podrška",
"LabelSettingsDateFormat": "Format datuma", "LabelSettingsDateFormat": "Format datuma",
"LabelSettingsDisableWatcher": "Isključi Watchera", "LabelSettingsDisableWatcher": "Isključi Watchera",
"LabelSettingsDisableWatcherForLibrary": "Isključi folder watchera za biblioteku", "LabelSettingsDisableWatcherForLibrary": "Isključi folder watchera za biblioteku",
"LabelSettingsDisableWatcherHelp": "Isključi automatsko dodavanje/aktualiziranje stavci ako su promjene prepoznate. *Potreban restart servera", "LabelSettingsDisableWatcherHelp": "Isključi automatsko dodavanje/aktualiziranje stavci ako su promjene prepoznate. *Potreban restart servera",
"LabelSettingsEnableEReader": "Uključi e-readere za sve korisnike",
"LabelSettingsEnableEReaderHelp": "E-reader je i dalje rad u tijeku, ali s ovom postavkom ga možete uključiti za sve korisnike (ili koristi \"Eksperimentalni features\" toggle da bi uključio postavku samo za sebe)",
"LabelSettingsExperimentalFeatures": "Eksperimentalni features", "LabelSettingsExperimentalFeatures": "Eksperimentalni features",
"LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.", "LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.",
"LabelSettingsFindCovers": "Pronađi covers", "LabelSettingsFindCovers": "Pronađi covers",
"LabelSettingsFindCoversHelp": "Ako audiobook nema embedani cover or a cover sliku unutar foldera, skener će probati pronaći cover.<br>Bilješka: Ovo će produžiti trjanje skeniranja", "LabelSettingsFindCoversHelp": "Ako audiobook nema embedani cover or a cover sliku unutar foldera, skener će probati pronaći cover.<br>Bilješka: Ovo će produžiti trjanje skeniranja",
"LabelSettingsHideSingleBookSeries": "Hide single book series",
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu", "LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
"LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku", "LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
"LabelSettingsOverdriveMediaMarkers": "Koristi Overdrive Media Markers za poglavlja", "LabelSettingsOverdriveMediaMarkers": "Koristi Overdrive Media Markers za poglavlja",
@@ -418,6 +453,9 @@
"LabelTagsAccessibleToUser": "Tags dostupni korisniku", "LabelTagsAccessibleToUser": "Tags dostupni korisniku",
"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": "Vremena odslušano", "LabelTimeListened": "Vremena odslušano",
"LabelTimeListenedToday": "Vremena odslušano danas", "LabelTimeListenedToday": "Vremena odslušano danas",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.", "MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.",
"MessageCheckingCron": "Provjeravam cron...", "MessageCheckingCron": "Provjeravam cron...",
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?", "MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?", "MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?", "MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?", "MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
"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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Preuzimam epizodu", "MessageDownloadingEpisode": "Preuzimam epizodu",
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.", "MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
"MessageEmbedFinished": "Embed završen!", "MessageEmbedFinished": "Embed završen!",
@@ -512,6 +554,8 @@
"MessageM4BFailed": "M4B neuspješan!", "MessageM4BFailed": "M4B neuspješan!",
"MessageM4BFinished": "M4B završio!", "MessageM4BFinished": "M4B završio!",
"MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.", "MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
"MessageMarkAsFinished": "Označi kao završeno", "MessageMarkAsFinished": "Označi kao završeno",
"MessageMarkAsNotFinished": "Označi kao nezavršeno", "MessageMarkAsNotFinished": "Označi kao nezavršeno",
"MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.", "MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
@@ -552,7 +596,6 @@
"MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePlaylistCreateFromCollection": "Create playlist from collection",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
"MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.", "MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
"MessageRemoveAllItemsWarning": "UPOZORENJE! Ova radnja briše sve stavke iz biblioteke uključujući bilokakve aktualizacije ili matcheve. Ovo ne mjenja vaše lokalne datoteke. Jeste li sigurni?",
"MessageRemoveChapter": "Remove chapter", "MessageRemoveChapter": "Remove chapter",
"MessageRemoveEpisodes": "ukloni {0} epizoda/-e", "MessageRemoveEpisodes": "ukloni {0} epizoda/-e",
"MessageRemoveFromPlayerQueue": "Remove from player queue", "MessageRemoveFromPlayerQueue": "Remove from player queue",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije", "ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda", "ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren", "ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Neuspješno brisanje serije", "ToastSessionDeleteFailed": "Neuspješno brisanje serije",
+70 -25
View File
@@ -55,7 +55,7 @@
"ButtonRemoveAll": "Rimuovi Tutto", "ButtonRemoveAll": "Rimuovi Tutto",
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria", "ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto", "ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading", "ButtonRemoveFromContinueReading": "Rimuovi per proseguire la lettura",
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla", "ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
"ButtonReScan": "Ri-scansiona", "ButtonReScan": "Ri-scansiona",
"ButtonReset": "Reset", "ButtonReset": "Reset",
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B", "ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata", "ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
"ButtonSubmit": "Invia", "ButtonSubmit": "Invia",
"ButtonTest": "Test",
"ButtonUpload": "Carica", "ButtonUpload": "Carica",
"ButtonUploadBackup": "Carica Backup", "ButtonUploadBackup": "Carica Backup",
"ButtonUploadCover": "Carica Cover", "ButtonUploadCover": "Carica Cover",
@@ -94,10 +95,15 @@
"HeaderCollection": "Raccolta", "HeaderCollection": "Raccolta",
"HeaderCollectionItems": "Elementi della Raccolta", "HeaderCollectionItems": "Elementi della Raccolta",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Download Correnti",
"HeaderDetails": "Dettagli", "HeaderDetails": "Dettagli",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodi", "HeaderEpisodes": "Episodi",
"HeaderEreaderDevices": "Dispositivo Ereader",
"HeaderEreaderSettings": "Impostazioni Ereader",
"HeaderFiles": "File", "HeaderFiles": "File",
"HeaderFindChapters": "Trova Capitoli", "HeaderFindChapters": "Trova Capitoli",
"HeaderIgnoredFiles": "File Ignorati", "HeaderIgnoredFiles": "File Ignorati",
@@ -143,12 +149,13 @@
"HeaderSettingsGeneral": "Generale", "HeaderSettingsGeneral": "Generale",
"HeaderSettingsScanner": "Scanner", "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Sveglia", "HeaderSleepTimer": "Sveglia",
"HeaderStatsLargestItems": "Largest Items", "HeaderStatsLargestItems": "Oggetti Grandi",
"HeaderStatsLongestItems": "libri più lunghi (ore)", "HeaderStatsLongestItems": "libri più lunghi (ore)",
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)", "HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
"HeaderStatsRecentSessions": "Sessioni Recenti", "HeaderStatsRecentSessions": "Sessioni Recenti",
"HeaderStatsTop10Authors": "Top 10 Autori", "HeaderStatsTop10Authors": "Top 10 Autori",
"HeaderStatsTop5Genres": "Top 5 Generi", "HeaderStatsTop5Genres": "Top 5 Generi",
"HeaderTableOfContents": "Tabellla dei Contenuti",
"HeaderTools": "Strumenti", "HeaderTools": "Strumenti",
"HeaderUpdateAccount": "Aggiorna Account", "HeaderUpdateAccount": "Aggiorna Account",
"HeaderUpdateAuthor": "Aggiorna Autore", "HeaderUpdateAuthor": "Aggiorna Autore",
@@ -156,13 +163,13 @@
"HeaderUpdateLibrary": "Aggiorna Libreria", "HeaderUpdateLibrary": "Aggiorna Libreria",
"HeaderUsers": "Utenti", "HeaderUsers": "Utenti",
"HeaderYourStats": "Statistiche Personali", "HeaderYourStats": "Statistiche Personali",
"LabelAbridged": "Abridged", "LabelAbridged": "Abbreviato",
"LabelAccountType": "Tipo di Account", "LabelAccountType": "Tipo di Account",
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Ospite", "LabelAccountTypeGuest": "Ospite",
"LabelAccountTypeUser": "Utente", "LabelAccountTypeUser": "Utente",
"LabelActivity": "Attività", "LabelActivity": "Attività",
"LabelAdded": "Added", "LabelAdded": "Aggiunto",
"LabelAddedAt": "Aggiunto il", "LabelAddedAt": "Aggiunto il",
"LabelAddToCollection": "Aggiungi alla Raccolta", "LabelAddToCollection": "Aggiungi alla Raccolta",
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
@@ -187,8 +194,8 @@
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Libri", "LabelBooks": "Libri",
"LabelChangePassword": "Cambia Password", "LabelChangePassword": "Cambia Password",
"LabelChannels": "Channels", "LabelChannels": "Canali",
"LabelChapters": "Chapters", "LabelChapters": "Capitoli",
"LabelChaptersFound": "Capitoli Trovati", "LabelChaptersFound": "Capitoli Trovati",
"LabelChapterTitle": "Titoli dei Capitoli", "LabelChapterTitle": "Titoli dei Capitoli",
"LabelClosePlayer": "Chiudi player", "LabelClosePlayer": "Chiudi player",
@@ -198,7 +205,7 @@
"LabelComplete": "Completo", "LabelComplete": "Completo",
"LabelConfirmPassword": "Conferma Password", "LabelConfirmPassword": "Conferma Password",
"LabelContinueListening": "Continua ad Ascoltare", "LabelContinueListening": "Continua ad Ascoltare",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "Continua la Lettura",
"LabelContinueSeries": "Continua Serie", "LabelContinueSeries": "Continua Serie",
"LabelCover": "Cover", "LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL", "LabelCoverImageURL": "Cover Image URL",
@@ -216,16 +223,24 @@
"LabelDiscFromFilename": "Disco dal nome file", "LabelDiscFromFilename": "Disco dal nome file",
"LabelDiscFromMetadata": "Disco dal Metadata", "LabelDiscFromMetadata": "Disco dal Metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Durata", "LabelDuration": "Durata",
"LabelDurationFound": "Durata Trovata:", "LabelDurationFound": "Durata Trovata:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Modifica", "LabelEdit": "Modifica",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Da Indirizzo",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Indirizzo",
"LabelEmbeddedCover": "Cover Integrata",
"LabelEnable": "Abilita", "LabelEnable": "Abilita",
"LabelEnd": "Fine", "LabelEnd": "Fine",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
"LabelEpisodeTitle": "Titolo Episodio", "LabelEpisodeTitle": "Titolo Episodio",
"LabelEpisodeType": "Tipo Episodio", "LabelEpisodeType": "Tipo Episodio",
"LabelExample": "Example", "LabelExample": "Esempio",
"LabelExplicit": "Esplicito", "LabelExplicit": "Esplicito",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFile": "File", "LabelFile": "File",
@@ -237,10 +252,14 @@
"LabelFinished": "Finita", "LabelFinished": "Finita",
"LabelFolder": "Cartella", "LabelFolder": "Cartella",
"LabelFolders": "Cartelle", "LabelFolders": "Cartelle",
"LabelFormat": "Format", "LabelFontScale": "Dimensione Font",
"LabelFormat": "Formato",
"LabelGenre": "Genere", "LabelGenre": "Genere",
"LabelGenres": "Generi", "LabelGenres": "Generi",
"LabelHardDeleteFile": "Elimina Definitivamente", "LabelHardDeleteFile": "Elimina Definitivamente",
"LabelHasEbook": "Un ebook",
"LabelHasSupplementaryEbook": "Un ebook Supplementare",
"LabelHost": "Host",
"LabelHour": "Ora", "LabelHour": "Ora",
"LabelIcon": "Icona", "LabelIcon": "Icona",
"LabelIncludeInTracklist": "Includi nella Tracklist", "LabelIncludeInTracklist": "Includi nella Tracklist",
@@ -256,21 +275,25 @@
"LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryDay": "Ogni Giorno",
"LabelIntervalEveryHour": "Ogni ora", "LabelIntervalEveryHour": "Ogni ora",
"LabelInvalidParts": "Parti Invalide", "LabelInvalidParts": "Parti Invalide",
"LabelInvert": "Invert", "LabelInvert": "Inverti",
"LabelItem": "Oggetti", "LabelItem": "Oggetti",
"LabelLanguage": "Lingua", "LabelLanguage": "Lingua",
"LabelLanguageDefaultServer": "Lingua di Default", "LabelLanguageDefaultServer": "Lingua di Default",
"LabelLastBookAdded": "Last Book Added", "LabelLastBookAdded": "Ultimo Libro Aggiunto",
"LabelLastBookUpdated": "Last Book Updated", "LabelLastBookUpdated": "Ultimo Libro Aggiornato",
"LabelLastSeen": "Ultimi Visti", "LabelLastSeen": "Ultimi Visti",
"LabelLastTime": "Ultima Volta", "LabelLastTime": "Ultima Volta",
"LabelLastUpdate": "Ultimo Aggiornamento", "LabelLastUpdate": "Ultimo Aggiornamento",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Pagina Singola",
"LabelLayoutSplitPage": "DIvidi Pagina",
"LabelLess": "Poco", "LabelLess": "Poco",
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti", "LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
"LabelLibrary": "Libreria", "LabelLibrary": "Libreria",
"LabelLibraryItem": "Elementi della Library", "LabelLibraryItem": "Elementi della Library",
"LabelLibraryName": "Nome Libreria", "LabelLibraryName": "Nome Libreria",
"LabelLimit": "Limiti", "LabelLimit": "Limiti",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Ri-ascolta", "LabelListenAgain": "Ri-ascolta",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
@@ -285,7 +308,7 @@
"LabelMissing": "Altro", "LabelMissing": "Altro",
"LabelMissingParts": "Parti rimantenti", "LabelMissingParts": "Parti rimantenti",
"LabelMore": "Molto", "LabelMore": "Molto",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "Più Info",
"LabelName": "Nome", "LabelName": "Nome",
"LabelNarrator": "Narratore", "LabelNarrator": "Narratore",
"LabelNarrators": "Narratori", "LabelNarrators": "Narratori",
@@ -295,6 +318,7 @@
"LabelNewPassword": "Nuova Password", "LabelNewPassword": "Nuova Password",
"LabelNextBackupDate": "Data Prossimo Backup", "LabelNextBackupDate": "Data Prossimo Backup",
"LabelNextScheduledRun": "Data prossima esecuzione schedulata", "LabelNextScheduledRun": "Data prossima esecuzione schedulata",
"LabelNoEpisodesSelected": "Nessun Episodio Selezionato",
"LabelNotes": "Note", "LabelNotes": "Note",
"LabelNotFinished": "Da Completare", "LabelNotFinished": "Da Completare",
"LabelNotificationAppriseURL": "Apprendi URL(s)", "LabelNotificationAppriseURL": "Apprendi URL(s)",
@@ -325,15 +349,19 @@
"LabelPlayMethod": "Metodo di riproduzione", "LabelPlayMethod": "Metodo di riproduzione",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Timo di Podcast", "LabelPodcastType": "Tipo di Podcast",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google", "LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
"LabelPrimaryEbook": "Libri Principlae",
"LabelProgress": "Cominciati", "LabelProgress": "Cominciati",
"LabelProvider": "Provider", "LabelProvider": "Provider",
"LabelPubDate": "Data Pubblicazione", "LabelPubDate": "Data Pubblicazione",
"LabelPublisher": "Editore", "LabelPublisher": "Editore",
"LabelPublishYear": "Anno Pubblicazione", "LabelPublishYear": "Anno Pubblicazione",
"LabelReadAgain": "Read Again", "LabelRead": "Leggi",
"LabelReadAgain": "Leggi Ancora",
"LabelReadEbookWithoutProgress": "Leggi l'ebook senza mantenere i progressi",
"LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentlyAdded": "Aggiunti Recentemente",
"LabelRecentSeries": "Serie Recenti", "LabelRecentSeries": "Serie Recenti",
"LabelRecommended": "Raccomandati", "LabelRecommended": "Raccomandati",
@@ -350,22 +378,29 @@
"LabelSearchTitle": "Cerca Titolo", "LabelSearchTitle": "Cerca Titolo",
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN", "LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
"LabelSeason": "Stagione", "LabelSeason": "Stagione",
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
"LabelSendEbookToDevice": "Invia ebook a...",
"LabelSequence": "Sequenza", "LabelSequence": "Sequenza",
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Nome Serie", "LabelSeriesName": "Nome Serie",
"LabelSeriesProgress": "Cominciato", "LabelSeriesProgress": "Cominciato",
"LabelSetEbookAsPrimary": "Immposta come Primario",
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di ebook a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come ebook supplementari",
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno", "LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
"LabelSettingsChromecastSupport": "Supporto a Chromecast", "LabelSettingsChromecastSupport": "Supporto a Chromecast",
"LabelSettingsDateFormat": "Formato Data", "LabelSettingsDateFormat": "Formato Data",
"LabelSettingsDisableWatcher": "Disattiva Watcher", "LabelSettingsDisableWatcher": "Disattiva Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie", "LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie",
"LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server", "LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server",
"LabelSettingsEnableEReader": "Abilita e-reader for tutti gli Utenti",
"LabelSettingsEnableEReaderHelp": "L'e-reader è ancora un work in progress, ma usa questa impostazione per abilitarlo a tutti i tuoi utenti (o usa lo switch \"Funzionalità sperimentali\" solo per te)",
"LabelSettingsExperimentalFeatures": "Opzioni Sperimentali", "LabelSettingsExperimentalFeatures": "Opzioni Sperimentali",
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.", "LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
"LabelSettingsFindCovers": "Trova covers", "LabelSettingsFindCovers": "Trova covers",
"LabelSettingsFindCoversHelp": "Se il tuo audiolibro non ha una copertina incorporata o un'immagine di copertina all'interno della cartella, questa funzione tenterà di trovare una copertina.<br>Nota: aumenta il tempo di scansione", "LabelSettingsFindCoversHelp": "Se il tuo audiolibro non ha una copertina incorporata o un'immagine di copertina all'interno della cartella, questa funzione tenterà di trovare una copertina.<br>Nota: aumenta il tempo di scansione",
"LabelSettingsHideSingleBookSeries": "Nascondi una singola serie di libri",
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno", "LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno", "LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli", "LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
@@ -416,8 +451,11 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
"LabelTasks": "Processi in esecuzione", "LabelTasks": "Processi in esecuzione",
"LabelTheme": "Tema",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Tempo di Ascolto", "LabelTimeListened": "Tempo di Ascolto",
"LabelTimeListenedToday": "Tempo di Ascolto Oggi", "LabelTimeListenedToday": "Tempo di Ascolto Oggi",
@@ -438,7 +476,7 @@
"LabelTracksMultiTrack": "Multi-traccia", "LabelTracksMultiTrack": "Multi-traccia",
"LabelTracksSingleTrack": "Traccia-singola", "LabelTracksSingleTrack": "Traccia-singola",
"LabelType": "Tipo", "LabelType": "Tipo",
"LabelUnabridged": "Unabridged", "LabelUnabridged": "Integrale",
"LabelUnknown": "Sconosciuto", "LabelUnknown": "Sconosciuto",
"LabelUpdateCover": "Aggiornamento Cover", "LabelUpdateCover": "Aggiornamento Cover",
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza", "LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
@@ -477,16 +515,19 @@
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro", "MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
"MessageCheckingCron": "Controllo cron...", "MessageCheckingCron": "Controllo cron...",
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?", "MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
"MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?",
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?", "MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?", "MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?", "MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?", "MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?", "MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?", "MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?", "MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?", "MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.", "MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.", "MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".", "MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
"MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?",
"MessageDownloadingEpisode": "Download episodio in corso", "MessageDownloadingEpisode": "Download episodio in corso",
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto", "MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
"MessageEmbedFinished": "Incorporamento finito!", "MessageEmbedFinished": "Incorporamento finito!",
@@ -512,6 +554,8 @@
"MessageM4BFailed": "M4B Fallito!", "MessageM4BFailed": "M4B Fallito!",
"MessageM4BFinished": "M4B Finito!", "MessageM4BFinished": "M4B Finito!",
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp", "MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
"MessageMarkAllEpisodesFinished": "Segna tutti gli episodi come finiti",
"MessageMarkAllEpisodesNotFinished": "Segna tutti gli episodi come non finiti",
"MessageMarkAsFinished": "Segna come finito", "MessageMarkAsFinished": "Segna come finito",
"MessageMarkAsNotFinished": "Segna come da completare", "MessageMarkAsNotFinished": "Segna come da completare",
"MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.", "MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.",
@@ -552,7 +596,6 @@
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta", "MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match", "MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".", "MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
"MessageRemoveChapter": "Rimuovi Capitolo", "MessageRemoveChapter": "Rimuovi Capitolo",
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)", "MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
"MessageRemoveFromPlayerQueue": "Rimuovi dalla coda di riproduzione", "MessageRemoveFromPlayerQueue": "Rimuovi dalla coda di riproduzione",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta", "ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed", "ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed chiuso", "ToastRSSFeedCloseSuccess": "RSS feed chiuso",
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito", "ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
"ToastSeriesUpdateSuccess": "Serie Aggornate", "ToastSeriesUpdateSuccess": "Serie Aggornate",
"ToastSessionDeleteFailed": "Errore eliminazione sessione", "ToastSessionDeleteFailed": "Errore eliminazione sessione",
@@ -657,4 +702,4 @@
"ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastSocketFailedToConnect": "Socket non riesce a connettersi",
"ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteFailed": "Errore eliminazione utente",
"ToastUserDeleteSuccess": "Utente eliminato" "ToastUserDeleteSuccess": "Utente eliminato"
} }
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Start M4B-encoding", "ButtonStartM4BEncode": "Start M4B-encoding",
"ButtonStartMetadataEmbed": "Start insluiten metadata", "ButtonStartMetadataEmbed": "Start insluiten metadata",
"ButtonSubmit": "Indienen", "ButtonSubmit": "Indienen",
"ButtonTest": "Test",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload back-up", "ButtonUploadBackup": "Upload back-up",
"ButtonUploadCover": "Upload cover", "ButtonUploadCover": "Upload cover",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Huidige downloads", "HeaderCurrentDownloads": "Huidige downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij", "HeaderDownloadQueue": "Download-wachtrij",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Afleveringen", "HeaderEpisodes": "Afleveringen",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Bestanden", "HeaderFiles": "Bestanden",
"HeaderFindChapters": "Zoek hoofdstukken", "HeaderFindChapters": "Zoek hoofdstukken",
"HeaderIgnoredFiles": "Genegeerde bestanden", "HeaderIgnoredFiles": "Genegeerde bestanden",
@@ -149,6 +155,7 @@
"HeaderStatsRecentSessions": "Recente sessies", "HeaderStatsRecentSessions": "Recente sessies",
"HeaderStatsTop10Authors": "Top 10 auteurs", "HeaderStatsTop10Authors": "Top 10 auteurs",
"HeaderStatsTop5Genres": "Top 5 genres", "HeaderStatsTop5Genres": "Top 5 genres",
"HeaderTableOfContents": "Table of Contents",
"HeaderTools": "Tools", "HeaderTools": "Tools",
"HeaderUpdateAccount": "Account bijwerken", "HeaderUpdateAccount": "Account bijwerken",
"HeaderUpdateAuthor": "Auteur bijwerken", "HeaderUpdateAuthor": "Auteur bijwerken",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Schijf uit bestandsnaam", "LabelDiscFromFilename": "Schijf uit bestandsnaam",
"LabelDiscFromMetadata": "Schijf uit metadata", "LabelDiscFromMetadata": "Schijf uit metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duur", "LabelDuration": "Duur",
"LabelDurationFound": "Gevonden duur:", "LabelDurationFound": "Gevonden duur:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Wijzig", "LabelEdit": "Wijzig",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Ingesloten cover", "LabelEmbeddedCover": "Ingesloten cover",
"LabelEnable": "Inschakelen", "LabelEnable": "Inschakelen",
"LabelEnd": "Einde", "LabelEnd": "Einde",
@@ -237,10 +252,14 @@
"LabelFinished": "Voltooid", "LabelFinished": "Voltooid",
"LabelFolder": "Map", "LabelFolder": "Map",
"LabelFolders": "Mappen", "LabelFolders": "Mappen",
"LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard-delete bestand", "LabelHardDeleteFile": "Hard-delete bestand",
"LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHost": "Host",
"LabelHour": "Uur", "LabelHour": "Uur",
"LabelIcon": "Icoon", "LabelIcon": "Icoon",
"LabelIncludeInTracklist": "Includeer in tracklijst", "LabelIncludeInTracklist": "Includeer in tracklijst",
@@ -265,12 +284,16 @@
"LabelLastSeen": "Laatst gezien", "LabelLastSeen": "Laatst gezien",
"LabelLastTime": "Laatste keer", "LabelLastTime": "Laatste keer",
"LabelLastUpdate": "Laatste update", "LabelLastUpdate": "Laatste update",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Single page",
"LabelLayoutSplitPage": "Split page",
"LabelLess": "Minder", "LabelLess": "Minder",
"LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken", "LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken",
"LabelLibrary": "Bibliotheek", "LabelLibrary": "Bibliotheek",
"LabelLibraryItem": "Library Item", "LabelLibraryItem": "Library Item",
"LabelLibraryName": "Library Name", "LabelLibraryName": "Library Name",
"LabelLimit": "Limiet", "LabelLimit": "Limiet",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Luister opnieuw", "LabelListenAgain": "Luister opnieuw",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
@@ -295,6 +318,7 @@
"LabelNewPassword": "Nieuw wachtwoord", "LabelNewPassword": "Nieuw wachtwoord",
"LabelNextBackupDate": "Volgende back-up datum", "LabelNextBackupDate": "Volgende back-up datum",
"LabelNextScheduledRun": "Volgende geplande run", "LabelNextScheduledRun": "Volgende geplande run",
"LabelNoEpisodesSelected": "No episodes selected",
"LabelNotes": "Notities", "LabelNotes": "Notities",
"LabelNotFinished": "Niet Voltooid", "LabelNotFinished": "Niet Voltooid",
"LabelNotificationAppriseURL": "Apprise URL(s)", "LabelNotificationAppriseURL": "Apprise URL(s)",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcasttype", "LabelPodcastType": "Podcasttype",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)", "LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
"LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen", "LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen",
"LabelPrimaryEbook": "Primary ebook",
"LabelProgress": "Voortgang", "LabelProgress": "Voortgang",
"LabelProvider": "Bron", "LabelProvider": "Bron",
"LabelPubDate": "Publicatiedatum", "LabelPubDate": "Publicatiedatum",
"LabelPublisher": "Uitgever", "LabelPublisher": "Uitgever",
"LabelPublishYear": "Jaar van uitgave", "LabelPublishYear": "Jaar van uitgave",
"LabelRead": "Read",
"LabelReadAgain": "Read Again", "LabelReadAgain": "Read Again",
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
"LabelRecentlyAdded": "Recent toegevoegd", "LabelRecentlyAdded": "Recent toegevoegd",
"LabelRecentSeries": "Recente series", "LabelRecentSeries": "Recente series",
"LabelRecommended": "Aangeraden", "LabelRecommended": "Aangeraden",
@@ -350,22 +378,29 @@
"LabelSearchTitle": "Zoek titel", "LabelSearchTitle": "Zoek titel",
"LabelSearchTitleOrASIN": "Zoek titel of ASIN", "LabelSearchTitleOrASIN": "Zoek titel of ASIN",
"LabelSeason": "Seizoen", "LabelSeason": "Seizoen",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequentie", "LabelSequence": "Sequentie",
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Naam serie", "LabelSeriesName": "Naam serie",
"LabelSeriesProgress": "Voortgang serie", "LabelSeriesProgress": "Voortgang serie",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken", "LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
"LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsChromecastSupport": "Chromecast support",
"LabelSettingsDateFormat": "Datum format", "LabelSettingsDateFormat": "Datum format",
"LabelSettingsDisableWatcher": "Watcher uitschakelen", "LabelSettingsDisableWatcher": "Watcher uitschakelen",
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen", "LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server", "LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
"LabelSettingsEnableEReader": "E-reader inschakelen voor alle gebruikers",
"LabelSettingsEnableEReaderHelp": "E-reader is nog in ontwikkeling, maar gebruik deze instelling om het beschikbaar te maken voor al je gebruikers (of gebruik de \"Experimentele functies\"-schakelaar voor eigen gebruik)",
"LabelSettingsExperimentalFeatures": "Experimentele functies", "LabelSettingsExperimentalFeatures": "Experimentele functies",
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.", "LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
"LabelSettingsFindCovers": "Zoek covers", "LabelSettingsFindCovers": "Zoek covers",
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen", "LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
"LabelSettingsHideSingleBookSeries": "Hide single book series",
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina", "LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek", "LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken", "LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
@@ -418,6 +453,9 @@
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker", "LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker", "LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
"LabelTasks": "Lopende taken", "LabelTasks": "Lopende taken",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelTimeBase": "Tijdsbasis", "LabelTimeBase": "Tijdsbasis",
"LabelTimeListened": "Tijd geluisterd", "LabelTimeListened": "Tijd geluisterd",
"LabelTimeListenedToday": "Tijd geluisterd vandaag", "LabelTimeListenedToday": "Tijd geluisterd vandaag",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek", "MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek",
"MessageCheckingCron": "Cron aan het checken...", "MessageCheckingCron": "Cron aan het checken...",
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?", "MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?", "MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?", "MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?", "MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?", "MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?", "MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?", "MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?", "MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.", "MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".", "MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Aflevering aan het dowloaden", "MessageDownloadingEpisode": "Aflevering aan het dowloaden",
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde", "MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
"MessageEmbedFinished": "Insluiting voltooid!", "MessageEmbedFinished": "Insluiting voltooid!",
@@ -512,6 +554,8 @@
"MessageM4BFailed": "M4B mislukt!", "MessageM4BFailed": "M4B mislukt!",
"MessageM4BFinished": "M4B voltooid!", "MessageM4BFinished": "M4B voltooid!",
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden", "MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
"MessageMarkAsFinished": "Markeer als Voltooid", "MessageMarkAsFinished": "Markeer als Voltooid",
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid", "MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.", "MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
@@ -552,7 +596,6 @@
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie", "MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching", "MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.", "MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
"MessageRemoveAllItemsWarning": "WAARSCHUWING! Deze actie zal alle onderdelen in de bibliotheek verwijderen uit de database, inclusief enige bijwerkingen of matches die je hebt gemaakt. Dit doet niets met je onderliggende bestanden. Weet je het zeker?",
"MessageRemoveChapter": "Verwijder hoofdstuk", "MessageRemoveChapter": "Verwijder hoofdstuk",
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)", "MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij", "MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie", "ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt", "ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten", "ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt", "ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt", "ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt", "ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
+48 -3
View File
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Eksportuj jako plik M4B", "ButtonStartM4BEncode": "Eksportuj jako plik M4B",
"ButtonStartMetadataEmbed": "Osadź metadane", "ButtonStartMetadataEmbed": "Osadź metadane",
"ButtonSubmit": "Zaloguj", "ButtonSubmit": "Zaloguj",
"ButtonTest": "Test",
"ButtonUpload": "Wgraj", "ButtonUpload": "Wgraj",
"ButtonUploadBackup": "Wgraj kopię zapasową", "ButtonUploadBackup": "Wgraj kopię zapasową",
"ButtonUploadCover": "Wgraj okładkę", "ButtonUploadCover": "Wgraj okładkę",
@@ -97,7 +98,12 @@
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Szczegóły", "HeaderDetails": "Szczegóły",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Rozdziały", "HeaderEpisodes": "Rozdziały",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Pliki", "HeaderFiles": "Pliki",
"HeaderFindChapters": "Wyszukaj rozdziały", "HeaderFindChapters": "Wyszukaj rozdziały",
"HeaderIgnoredFiles": "Zignoruj pliki", "HeaderIgnoredFiles": "Zignoruj pliki",
@@ -149,6 +155,7 @@
"HeaderStatsRecentSessions": "Ostatnie sesje", "HeaderStatsRecentSessions": "Ostatnie sesje",
"HeaderStatsTop10Authors": "Top 10 Autorów", "HeaderStatsTop10Authors": "Top 10 Autorów",
"HeaderStatsTop5Genres": "Top 5 Gatunków", "HeaderStatsTop5Genres": "Top 5 Gatunków",
"HeaderTableOfContents": "Table of Contents",
"HeaderTools": "Narzędzia", "HeaderTools": "Narzędzia",
"HeaderUpdateAccount": "Zaktualizuj konto", "HeaderUpdateAccount": "Zaktualizuj konto",
"HeaderUpdateAuthor": "Zaktualizuj autorów", "HeaderUpdateAuthor": "Zaktualizuj autorów",
@@ -216,9 +223,17 @@
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku", "LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych", "LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
"LabelDownload": "Pobierz", "LabelDownload": "Pobierz",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Czas trwania", "LabelDuration": "Czas trwania",
"LabelDurationFound": "Znaleziona długość:", "LabelDurationFound": "Znaleziona długość:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Edytuj", "LabelEdit": "Edytuj",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "From Address",
"LabelEmailSettingsSecure": "Secure",
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Włącz", "LabelEnable": "Włącz",
"LabelEnd": "Zakończ", "LabelEnd": "Zakończ",
@@ -237,10 +252,14 @@
"LabelFinished": "Zakończone", "LabelFinished": "Zakończone",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Foldery", "LabelFolders": "Foldery",
"LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Gatunek", "LabelGenre": "Gatunek",
"LabelGenres": "Gatunki", "LabelGenres": "Gatunki",
"LabelHardDeleteFile": "Usuń trwale plik", "LabelHardDeleteFile": "Usuń trwale plik",
"LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHost": "Host",
"LabelHour": "Godzina", "LabelHour": "Godzina",
"LabelIcon": "Ikona", "LabelIcon": "Ikona",
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania", "LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
@@ -265,12 +284,16 @@
"LabelLastSeen": "Ostatnio widziany", "LabelLastSeen": "Ostatnio widziany",
"LabelLastTime": "Ostatni czas", "LabelLastTime": "Ostatni czas",
"LabelLastUpdate": "Ostatnia aktualizacja", "LabelLastUpdate": "Ostatnia aktualizacja",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Single page",
"LabelLayoutSplitPage": "Split page",
"LabelLess": "Mniej", "LabelLess": "Mniej",
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika", "LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
"LabelLibrary": "Biblioteka", "LabelLibrary": "Biblioteka",
"LabelLibraryItem": "Element biblioteki", "LabelLibraryItem": "Element biblioteki",
"LabelLibraryName": "Nazwa biblioteki", "LabelLibraryName": "Nazwa biblioteki",
"LabelLimit": "Limit", "LabelLimit": "Limit",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Słuchaj ponownie", "LabelListenAgain": "Słuchaj ponownie",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Informacja", "LabelLogLevelInfo": "Informacja",
@@ -295,6 +318,7 @@
"LabelNewPassword": "Nowe hasło", "LabelNewPassword": "Nowe hasło",
"LabelNextBackupDate": "Next backup date", "LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run", "LabelNextScheduledRun": "Next scheduled run",
"LabelNoEpisodesSelected": "No episodes selected",
"LabelNotes": "Uwagi", "LabelNotes": "Uwagi",
"LabelNotFinished": "Nieukończone", "LabelNotFinished": "Nieukończone",
"LabelNotificationAppriseURL": "URLe Apprise", "LabelNotificationAppriseURL": "URLe Apprise",
@@ -326,14 +350,18 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)", "LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
"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": "Postęp", "LabelProgress": "Postęp",
"LabelProvider": "Dostawca", "LabelProvider": "Dostawca",
"LabelPubDate": "Data publikacji", "LabelPubDate": "Data publikacji",
"LabelPublisher": "Wydawca", "LabelPublisher": "Wydawca",
"LabelPublishYear": "Rok publikacji", "LabelPublishYear": "Rok publikacji",
"LabelRead": "Read",
"LabelReadAgain": "Read Again", "LabelReadAgain": "Read Again",
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
"LabelRecentlyAdded": "Niedawno dodany", "LabelRecentlyAdded": "Niedawno dodany",
"LabelRecentSeries": "Ostatnie serie", "LabelRecentSeries": "Ostatnie serie",
"LabelRecommended": "Recommended", "LabelRecommended": "Recommended",
@@ -350,22 +378,29 @@
"LabelSearchTitle": "Wyszukaj tytuł", "LabelSearchTitle": "Wyszukaj tytuł",
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN", "LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
"LabelSeason": "Sezon", "LabelSeason": "Sezon",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Kolejność", "LabelSequence": "Kolejność",
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Nazwy serii", "LabelSeriesName": "Nazwy serii",
"LabelSeriesProgress": "Postęp w serii", "LabelSeriesProgress": "Postęp w serii",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
"LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami", "LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami",
"LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsChromecastSupport": "Wsparcie Chromecast",
"LabelSettingsDateFormat": "Format daty", "LabelSettingsDateFormat": "Format daty",
"LabelSettingsDisableWatcher": "Wyłącz monitorowanie", "LabelSettingsDisableWatcher": "Wyłącz monitorowanie",
"LabelSettingsDisableWatcherForLibrary": "Wyłącz monitorowanie folderów dla biblioteki", "LabelSettingsDisableWatcherForLibrary": "Wyłącz monitorowanie folderów dla biblioteki",
"LabelSettingsDisableWatcherHelp": "Wyłącz automatyczne dodawanie/aktualizowanie elementów po wykryciu zmian w plikach. *Wymaga restartu serwera", "LabelSettingsDisableWatcherHelp": "Wyłącz automatyczne dodawanie/aktualizowanie elementów po wykryciu zmian w plikach. *Wymaga restartu serwera",
"LabelSettingsEnableEReader": "Włącz e-czytnika dla wszystkich użytkowników",
"LabelSettingsEnableEReaderHelp": "E-czytnik jest wciąż w fazie rozwoju, ale użyj tego ustawienia, aby udostępnić go wszystkim użytkownikom (lub użyj przełącznika \"Funkcje eksperymentalne\" aby włączyć funkcję tylko dla Ciebie)",
"LabelSettingsExperimentalFeatures": "Funkcje eksperymentalne", "LabelSettingsExperimentalFeatures": "Funkcje eksperymentalne",
"LabelSettingsExperimentalFeaturesHelp": "Funkcje w trakcie rozwoju, które mogą zyskanć na Twojej opinii i pomocy w testowaniu. Kliknij, aby otworzyć dyskusję na githubie.", "LabelSettingsExperimentalFeaturesHelp": "Funkcje w trakcie rozwoju, które mogą zyskanć na Twojej opinii i pomocy w testowaniu. Kliknij, aby otworzyć dyskusję na githubie.",
"LabelSettingsFindCovers": "Szukanie okładek", "LabelSettingsFindCovers": "Szukanie okładek",
"LabelSettingsFindCoversHelp": "Jeśli audiobook nie posiada zintegrowanej okładki albo w folderze nie zostanie znaleziony plik okładki, skaner podejmie próbę pobrania okładki z sieci. <br>Uwaga: może to wydłuzyć proces skanowania", "LabelSettingsFindCoversHelp": "Jeśli audiobook nie posiada zintegrowanej okładki albo w folderze nie zostanie znaleziony plik okładki, skaner podejmie próbę pobrania okładki z sieci. <br>Uwaga: może to wydłuzyć proces skanowania",
"LabelSettingsHideSingleBookSeries": "Hide single book series",
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej", "LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki", "LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
"LabelSettingsOverdriveMediaMarkers": "Użyj markerów Overdrive Media Markers dla rozdziałów", "LabelSettingsOverdriveMediaMarkers": "Użyj markerów Overdrive Media Markers dla rozdziałów",
@@ -418,6 +453,9 @@
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika", "LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
"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": "Czas odtwarzania", "LabelTimeListened": "Czas odtwarzania",
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj", "LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
@@ -477,9 +515,12 @@
"MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka", "MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka",
"MessageCheckingCron": "Sprawdzanie cron...", "MessageCheckingCron": "Sprawdzanie cron...",
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?", "MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?", "MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?", "MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?", "MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "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?",
@@ -494,6 +535,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Pobieranie odcinka", "MessageDownloadingEpisode": "Pobieranie odcinka",
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów", "MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
"MessageEmbedFinished": "Osadzanie zakończone!", "MessageEmbedFinished": "Osadzanie zakończone!",
@@ -512,6 +554,8 @@
"MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się", "MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się",
"MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!", "MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!",
"MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu", "MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu",
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
"MessageMarkAsFinished": "Oznacz jako ukończone", "MessageMarkAsFinished": "Oznacz jako ukończone",
"MessageMarkAsNotFinished": "Oznacz jako nieukończone", "MessageMarkAsNotFinished": "Oznacz jako nieukończone",
"MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.", "MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.",
@@ -552,7 +596,6 @@
"MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePlaylistCreateFromCollection": "Create playlist from collection",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.", "MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
"MessageRemoveAllItemsWarning": "UWAGA! Ta akcja usunie wszystkie elementy biblioteki z bazy danych, w tym wszystkie aktualizacje lub dopasowania, które zostały wykonane. Pliki pozostaną niezmienione. Czy jesteś pewien?",
"MessageRemoveChapter": "Usuń rozdział", "MessageRemoveChapter": "Usuń rozdział",
"MessageRemoveEpisodes": "Usuń {0} odcinków", "MessageRemoveEpisodes": "Usuń {0} odcinków",
"MessageRemoveFromPlayerQueue": "Remove from player queue", "MessageRemoveFromPlayerQueue": "Remove from player queue",
@@ -648,6 +691,8 @@
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji", "ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się", "ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji", "ToastSessionDeleteFailed": "Nie udało się usunąć sesji",

Some files were not shown because too many files have changed in this diff Show More