Compare commits

...

79 Commits

Author SHA1 Message Date
advplyr 1fe4cffd3b Version bump 2.2.4 2022-11-13 14:13:12 -06:00
advplyr 8f83752abc Fix:Get library items endpoint limit & total entities count 2022-11-13 13:25:20 -06:00
advplyr 31be2ba4fb Update:User getMostRecentItemProgress method to support podcast episode progress 2022-11-13 09:03:16 -06:00
advplyr dc156a2eac Update:api/users/online API endpoint unauth status code 2022-11-13 08:26:32 -06:00
advplyr 42050a5f17 Fix:User toJSONForPublic method 2022-11-13 08:25:51 -06:00
advplyr bcc7fcb645 Add:Polish translations, update translation json files with new strings, fix side rail buttons to center and wrap long text #1103 2022-11-13 08:15:41 -06:00
advplyr d96f427b83 Merge pull request #1149 from konradorlinski/polish-translation
Update polish translation
2022-11-13 07:57:41 -06:00
konradorlinski bba8d0a46f Update polish translation 2022-11-13 12:53:31 +01:00
advplyr a07a69e7de Version bump 2.2.3 2022-11-12 17:22:16 -06:00
advplyr cbc2f64e2e Add:Croatian language #1103 2022-11-12 16:55:42 -06:00
advplyr ef622108c9 Merge pull request #1147 from Smoukus/croatian-translation
add Croatian translation
2022-11-12 16:49:00 -06:00
advplyr 78559520ab Add:Player queue for audiobooks #1077 2022-11-12 16:48:35 -06:00
Smoukus 61a8f31802 add croatian translation 2022-11-12 23:45:40 +01:00
advplyr 3357ccfaf3 Add:Buttons to add/remove podcast episodes from player queue 2022-11-12 15:41:41 -06:00
advplyr 92e3e0ef6e Update collection id prefix 2022-11-12 14:31:45 -06:00
advplyr ed76f51f4b Update:Service worker icons 2022-11-12 10:03:41 -06:00
advplyr 7d569e1e3e Update:Some incorrect status codes returned from API 2022-11-12 09:36:00 -06:00
advplyr 16cf5b5616 Add:Italian language selection #1103 2022-11-12 08:07:53 -06:00
advplyr b260bcaeb1 Merge pull request #1144 from austinphilp/fix-listening-sessions-count-bug
Fix listening sessions count bug
2022-11-12 08:02:07 -06:00
advplyr 3ffc481a54 Fix users latest session computed property 2022-11-12 08:03:13 -06:00
advplyr b9b38d82f2 Merge pull request #1146 from burghy86/patch-2
Update it.json
2022-11-12 07:52:24 -06:00
advplyr 9635d72cef Update client/strings/it.json 2022-11-12 07:52:20 -06:00
burghy86 288edae3d1 Update it.json
surely there are various things to fix. I'll take the time next week to correct everything properly
2022-11-12 14:36:25 +01:00
advplyr ec90aafed1 Merge pull request #1145 from Smoukus/german-translation
fix minor typos
2022-11-12 04:31:58 -06:00
Smoukus c023678c11 fix minor typos 2022-11-12 11:18:24 +01:00
advplyr cada1a6857 Update:Add Deutsch language to dropdown #1103 2022-11-11 17:57:02 -06:00
advplyr 5eac2a91fb Merge pull request #1143 from Hallo951/master
german translation
2022-11-11 17:48:21 -06:00
Austin Philp eb295453fc Cleanup 2022-11-11 15:47:20 -08:00
advplyr 28feed6ea2 Fix:Remove collections when removing library 2022-11-11 17:44:19 -06:00
Austin Philp c6dc4054be Use total from listening-sessions endpoint to display total sessions 2022-11-11 15:41:50 -08:00
advplyr 6f901defd6 Fix:Show only collections for selected library #1130 2022-11-11 17:28:05 -06:00
advplyr 4cbc8676c6 Update:Rename UserCollections to Collections 2022-11-11 17:13:10 -06:00
Hallo951 0d587b6aae Übersetzung_final 2022-11-12 00:08:47 +01:00
Hallo951 a47bf7a835 Übersetzung_v4 2022-11-11 14:33:25 +01:00
Hallo951 fce9e72851 Übersetzung_v3 2022-11-11 14:22:55 +01:00
Hallo951 6357fb26bf Übersetzung_v2 2022-11-11 12:49:00 +01:00
Hallo951 d2aabde8fe Merge branch 'advplyr:master' into master 2022-11-11 08:30:54 +01:00
advplyr fdf67e17a0 Add:API endpoint to get users online and open listening sessions #1125 2022-11-10 17:42:20 -06:00
advplyr abb4137d4c Fix:Set library item updatedAt when scan has updates, fixes updating an open RSS feed #1131 2022-11-10 17:25:17 -06:00
advplyr a237058e30 Merge pull request #1134 from springsunx/patch-1
change language name and fix translation errors
2022-11-10 16:46:53 -06:00
SunX 06851f50f4 Update zh-cn.json 2022-11-10 21:46:05 +08:00
SunX 54c1a49e1e Update zh-cn.json 2022-11-10 20:14:05 +08:00
SunX 12e47fb034 Update zh-cn.json 2022-11-10 20:10:34 +08:00
SunX c91897ae99 Update zh-cn.json
Fix translation errors.
2022-11-10 19:51:54 +08:00
SunX 26f4479859 Update i18n.js
change langeage name
2022-11-10 10:46:31 +08:00
advplyr c33314edfb Add:Language select in account page #1103 2022-11-09 18:00:20 -06:00
advplyr b083f6ab96 Fix:Podcast quick match genres 2022-11-09 16:50:26 -06:00
advplyr 8d5e08b76a Merge pull request #1132 from springsunx/patch-1
Update zh-cn.json
2022-11-09 16:03:04 -06:00
Hallo951 a7019e2f11 Übersetzung_v1 2022-11-09 11:03:16 +01:00
Hallo951 a7163f7a00 Merge branch 'advplyr:master' into master 2022-11-09 09:50:09 +01:00
SunX a1f758cd7b Update zh-cn.json 2022-11-09 13:58:06 +08:00
advplyr 946e4f39cc merge translations 2022-11-08 18:11:03 -06:00
advplyr 6e064eeafb Add:Server setting for default language #1103 2022-11-08 18:09:07 -06:00
advplyr 400e34a4c7 Update:More localization strings #1103 2022-11-08 17:10:08 -06:00
Hallo951 780a0a9dd6 Übersetzung_v1 2022-11-08 22:20:10 +01:00
advplyr c1b3d7779b Fix:Multi-select and shift select 2022-11-08 08:38:42 -06:00
advplyr 2662b3ec49 Update:More localization strings #1103 2022-11-08 08:37:39 -06:00
advplyr 042a175d16 Merge pull request #1119 from burghy86/patch-1
Update it.json
2022-11-07 18:28:11 -06:00
advplyr 5e50ac91ff Merge pull request #1121 from ruoti/add-series-ranges
Fixing range generation in series labels
2022-11-07 18:27:26 -06:00
advplyr faac6f677a Update:More localization strings #1103 2022-11-07 18:27:17 -06:00
advplyr 46d02744a1 Merge pull request #1120 from springsunx/patch-1
Update zh-ch.json
2022-11-07 18:27:08 -06:00
advplyr d7e61c3aba Merge pull request #1124 from konradorlinski/pol-patch
Update polish translation
2022-11-07 18:26:44 -06:00
Konrad c23c51eb78 Update polish translation 2022-11-07 21:29:56 +01:00
burghy86 270b2bb826 Update it.json 2022-11-07 16:53:56 +01:00
Scott Ruoti 0643116e9b Fixing range generation in series labels 2022-11-07 09:24:48 -05:00
SunX 03ea055299 Update zh-ch.json 2022-11-07 22:22:55 +08:00
SunX da12f94be4 Update zh-ch.json 2022-11-07 21:25:39 +08:00
burghy86 64d196c347 Update it.json 2022-11-07 14:06:05 +01:00
burghy86 c8d3e0c912 Update it.json
i have traslate all item. but I see on my  portal that on the home page there are other objects to be translated.
will you add them soon? I mean the "Continue Listening", "Continue Series", "Recently Added" on the homepage, Listen Again, Newest Authors, collapse series, You haven't made any collections yet.
 in option zone not found: Filter by User, backup page, notification page, and your stat page
2022-11-07 14:00:28 +01:00
advplyr eb463a2958 Add:Start of localization i18n #1103 2022-11-06 17:56:44 -06:00
advplyr 3282ac67e4 Fix:Podcast pubDate parsing #1116 2022-11-06 15:43:17 -06:00
advplyr 8319891c96 Merge pull request #1105 from ruoti/collapseseries-patch
Patching handling of titles with multiple series
2022-11-06 10:55:38 -06:00
advplyr 24d97d17ba Add collapseBookSeries to default settings 2022-11-06 10:55:44 -06:00
advplyr 7425622d93 Merge branch 'master' into collapseseries-patch 2022-11-06 10:52:22 -06:00
advplyr 5050de3a17 Update deploy-linux script 2022-11-06 10:24:59 -06:00
Scott Ruoti b1111912f7 Added sorting by sequence for series and collapsing series in series view 2022-11-05 20:30:13 -04:00
Scott Ruoti c1035d97e8 Show book sequences for collapsed series when filtering by series 2022-11-05 20:01:01 -04:00
Scott Ruoti b322d0207b Fixed sorting to be more consistent for multiple series (and generally) 2022-11-05 20:01:01 -04:00
Scott Ruoti d64932dad7 Fixes bug when titles are in multiple series being collapsed 2022-11-05 20:01:01 -04:00
134 changed files with 5076 additions and 1389 deletions
+1
View File
@@ -13,3 +13,4 @@ test/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
/deploy/
+1
View File
@@ -11,6 +11,7 @@ test/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
/deploy/
sw.* sw.*
.DS_STORE .DS_STORE
+7 -8
View File
@@ -45,17 +45,16 @@
</span> </span>
</nuxt-link> </nuxt-link>
</div> </div>
<div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center"> <div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
<h1 class="text-2xl px-4">{{ numLibraryItemsSelected }} Selected</h1> <h1 class="text-2xl px-4">{{ $getString('MessageItemsSelected', [numLibraryItemsSelected]) }}</h1>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" text="Quick Match Selected" direction="bottom"> <ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" /> <ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcastLibrary" :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom"> <ui-tooltip v-if="!isPodcastLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" /> <ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" text="Add to Collection" direction="bottom"> <ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" /> <ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
</ui-tooltip> </ui-tooltip>
<template v-if="userCanUpdate && numLibraryItemsSelected < 50"> <template v-if="userCanUpdate && numLibraryItemsSelected < 50">
@@ -63,10 +62,10 @@
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" /> <ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
</ui-tooltip> </ui-tooltip>
</template> </template>
<ui-tooltip v-if="userCanDelete" text="Delete" direction="bottom"> <ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
<ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" /> <ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip text="Deselect All" direction="bottom"> <ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span> <span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -209,7 +208,7 @@ export default {
this.$router.push('/batch') this.$router.push('/batch')
}, },
batchAddToCollectionClick() { batchAddToCollectionClick() {
this.$store.commit('globals/setShowBatchUserCollectionsModal', true) this.$store.commit('globals/setShowBatchCollectionsModal', true)
}, },
setBookshelfTotalEntities(totalEntities) { setBookshelfTotalEntities(totalEntities) {
this.totalEntities = totalEntities this.totalEntities = totalEntities
+10 -5
View File
@@ -16,17 +16,17 @@
<!-- Alternate plain view --> <!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24"> <div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in shelves">
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)"> <widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p> <p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-item-slider> </widgets-item-slider>
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)"> <widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p> <p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-episode-slider> </widgets-episode-slider>
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6"> <widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p> <p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-series-slider> </widgets-series-slider>
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6"> <widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p> <p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-authors-slider> </widgets-authors-slider>
</template> </template>
</div> </div>
@@ -180,6 +180,7 @@ export default {
shelves.push({ shelves.push({
id: 'books', id: 'books',
label: 'Books', label: 'Books',
labelStringKey: 'LabelBooks',
type: 'book', type: 'book',
entities: this.results.books.map((res) => res.libraryItem) entities: this.results.books.map((res) => res.libraryItem)
}) })
@@ -189,6 +190,7 @@ export default {
shelves.push({ shelves.push({
id: 'podcasts', id: 'podcasts',
label: 'Podcasts', label: 'Podcasts',
labelStringKey: 'LabelPodcasts',
type: 'podcast', type: 'podcast',
entities: this.results.podcasts.map((res) => res.libraryItem) entities: this.results.podcasts.map((res) => res.libraryItem)
}) })
@@ -198,6 +200,7 @@ export default {
shelves.push({ shelves.push({
id: 'series', id: 'series',
label: 'Series', label: 'Series',
labelStringKey: 'LabelSeries',
type: 'series', type: 'series',
entities: this.results.series.map((seriesObj) => { entities: this.results.series.map((seriesObj) => {
return { return {
@@ -212,6 +215,7 @@ export default {
shelves.push({ shelves.push({
id: 'tags', id: 'tags',
label: 'Tags', label: 'Tags',
labelStringKey: 'LabelTags',
type: 'tags', type: 'tags',
entities: this.results.tags.map((tagObj) => { entities: this.results.tags.map((tagObj) => {
return { return {
@@ -226,6 +230,7 @@ export default {
shelves.push({ shelves.push({
id: 'authors', id: 'authors',
label: 'Authors', label: 'Authors',
labelStringKey: 'LabelAuthors',
type: 'authors', type: 'authors',
entities: this.results.authors.map((a) => { entities: this.results.authors.map((a) => {
return { return {
+1 -1
View File
@@ -46,7 +46,7 @@
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px"> <div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
<p class="transform text-sm">{{ shelf.label }}</p> <p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
</div> </div>
</div> </div>
+19 -15
View File
@@ -2,19 +2,19 @@
<div class="w-full h-20 md:h-10 relative"> <div class="w-full h-20 md:h-10 relative">
<div class="flex md:hidden h-10 items-center"> <div class="flex md:hidden h-10 items-center">
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Home</p> <p class="text-sm">{{ $strings.ButtonHome }}</p>
</nuxt-link> </nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Library</p> <p class="text-sm">{{ $strings.ButtonLibrary }}</p>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Series</p> <p class="text-sm">{{ $strings.ButtonSeries }}</p>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Collections</p> <p class="text-sm">{{ $strings.ButtonCollections }}</p>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Search</p> <p class="text-sm">{{ $strings.ButtonSearch }}</p>
</nuxt-link> </nuxt-link>
</div> </div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8"> <div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
@@ -28,7 +28,8 @@
<span class="font-mono">{{ numShowing }}</span> <span class="font-mono">{{ numShowing }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center" @click="markSeriesFinished"> <ui-checkbox v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4" @click="markSeriesFinished">
<div class="h-5 w-5"> <div class="h-5 w-5">
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)"> <svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
@@ -37,27 +38,27 @@
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" /> <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg> </svg>
</div> </div>
<span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span> <span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
</ui-btn> </ui-btn>
</div> </div>
<div class="flex-grow hidden sm:inline-block" /> <div class="flex-grow hidden sm:inline-block" />
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" /> <ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
<controls-library-filter-select v-if="isLibraryPage" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" /> <controls-library-filter-select v-if="isLibraryPage" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
<controls-library-sort-select v-if="isLibraryPage" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" /> <controls-library-sort-select v-if="isLibraryPage" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
<controls-library-filter-select v-if="isSeriesPage" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" /> <controls-library-filter-select v-if="isSeriesPage" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
<controls-sort-select v-if="isSeriesPage" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" /> <controls-sort-select v-if="isSeriesPage" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">Remove All {{ numShowing }} {{ entityName }}</ui-btn> <ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
</template> </template>
<template v-else-if="page === 'search'"> <template v-else-if="page === 'search'">
<div class="flex-grow" /> <div class="flex-grow" />
<p>Search results for "{{ searchQuery }}"</p> <p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
<div class="flex-grow" /> <div class="flex-grow" />
</template> </template>
<template v-else-if="page === 'authors'"> <template v-else-if="page === 'authors'">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">Match All Authors</ui-btn> <ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
</template> </template>
</div> </div>
</div> </div>
@@ -144,10 +145,10 @@ export default {
return this.totalEntities return this.totalEntities
}, },
entityName() { entityName() {
if (this.isPodcastLibrary) return 'Podcasts' if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
if (!this.page) return 'Books' if (!this.page) return this.$strings.LabelBooks
if (this.isSeriesPage) return 'Series' if (this.isSeriesPage) return this.$strings.LabelSeries
if (this.isCollectionsPage) return 'Collections' if (this.isCollectionsPage) return this.$strings.LabelCollections
return '' return ''
}, },
seriesName() { seriesName() {
@@ -280,6 +281,9 @@ export default {
updateCollapseSeries() { updateCollapseSeries() {
this.saveSettings() this.saveSettings()
}, },
updateCollapseBookSeries() {
this.saveSettings()
},
saveSettings() { saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings) this.$store.dispatch('user/updateUserSettings', this.settings)
}, },
+13 -13
View File
@@ -18,7 +18,7 @@
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div> </div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/> <modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
</div> </div>
</template> </template>
@@ -47,7 +47,7 @@ export default {
return [ return [
{ {
id: 'config-stats', id: 'config-stats',
title: 'Your Stats', title: this.$strings.HeaderYourStats,
path: '/config/stats' path: '/config/stats'
} }
] ]
@@ -55,37 +55,37 @@ export default {
const configRoutes = [ const configRoutes = [
{ {
id: 'config', id: 'config',
title: 'Settings', title: this.$strings.HeaderSettings,
path: '/config' path: '/config'
}, },
{ {
id: 'config-libraries', id: 'config-libraries',
title: 'Libraries', title: this.$strings.HeaderLibraries,
path: '/config/libraries' path: '/config/libraries'
}, },
{ {
id: 'config-users', id: 'config-users',
title: 'Users', title: this.$strings.HeaderUsers,
path: '/config/users' path: '/config/users'
}, },
{ {
id: 'config-sessions', id: 'config-sessions',
title: 'Listening Sessions', title: this.$strings.HeaderListeningSessions,
path: '/config/sessions' path: '/config/sessions'
}, },
{ {
id: 'config-backups', id: 'config-backups',
title: 'Backups', title: this.$strings.HeaderBackups,
path: '/config/backups' path: '/config/backups'
}, },
{ {
id: 'config-log', id: 'config-log',
title: 'Logs', title: this.$strings.HeaderLogs,
path: '/config/log' path: '/config/log'
}, },
{ {
id: 'config-notifications', id: 'config-notifications',
title: 'Notifications', title: this.$strings.HeaderNotifications,
path: '/config/notifications' path: '/config/notifications'
} }
] ]
@@ -93,12 +93,12 @@ export default {
if (this.currentLibraryId) { if (this.currentLibraryId) {
configRoutes.push({ configRoutes.push({
id: 'config-library-stats', id: 'config-library-stats',
title: 'Library Stats', title: this.$strings.HeaderLibraryStats,
path: '/config/library-stats' path: '/config/library-stats'
}) })
configRoutes.push({ configRoutes.push({
id: 'config-stats', id: 'config-stats',
title: 'Your Stats', title: this.$strings.HeaderYourStats,
path: '/config/stats' path: '/config/stats'
}) })
} }
@@ -146,7 +146,7 @@ export default {
} }
}, },
methods: { methods: {
clickChangelog(){ clickChangelog() {
this.showChangelogModal = true this.showChangelogModal = true
}, },
clickOutside() { clickOutside() {
@@ -155,7 +155,7 @@ export default {
}, },
closeDrawer() { closeDrawer() {
this.$emit('update:isOpen', false) this.$emit('update:isOpen', false)
}, }
} }
} }
</script> </script>
+11 -5
View File
@@ -119,6 +119,9 @@ export default {
collapseSeries() { collapseSeries() {
return this.$store.getters['user/getUserSetting']('collapseSeries') return this.$store.getters['user/getUserSetting']('collapseSeries')
}, },
collapseBookSeries() {
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
},
coverAspectRatio() { coverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
@@ -319,7 +322,6 @@ export default {
this.totalEntities = payload.total this.totalEntities = payload.total
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf) this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
this.entities = new Array(this.totalEntities) this.entities = new Array(this.totalEntities)
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
} }
for (let i = 0; i < payload.results.length; i++) { for (let i = 0; i < payload.results.length; i++) {
@@ -329,6 +331,8 @@ export default {
this.entityComponentRefs[index].setEntity(this.entities[index]) this.entityComponentRefs[index].setEntity(this.entities[index])
} }
} }
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
} }
}, },
loadPage(page) { loadPage(page) {
@@ -437,6 +441,9 @@ export default {
searchParams.set('filter', this.seriesFilterBy) searchParams.set('filter', this.seriesFilterBy)
} else if (this.page === 'series-books') { } else if (this.page === 'series-books') {
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`) searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
if (this.collapseBookSeries) {
searchParams.set('collapseseries', 1)
}
} else { } else {
if (this.filterBy && this.filterBy !== 'all') { if (this.filterBy && this.filterBy !== 'all') {
searchParams.set('filter', this.filterBy) searchParams.set('filter', this.filterBy)
@@ -452,8 +459,6 @@ export default {
return searchParams.toString() return searchParams.toString()
}, },
checkUpdateSearchParams() { checkUpdateSearchParams() {
if (this.page === 'series-books') return false
var newSearchParams = this.buildSearchParams() var newSearchParams = this.buildSearchParams()
var currentQueryString = window.location.search var currentQueryString = window.location.search
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1) if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
@@ -512,7 +517,7 @@ export default {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id) var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
if (indexOf >= 0) { if (indexOf >= 0) {
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id) this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
this.totalEntities = this.entities.length this.totalEntities--
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities) this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
this.executeRebuild() this.executeRebuild()
} }
@@ -550,7 +555,7 @@ export default {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === collection.id) var indexOf = this.entities.findIndex((ent) => ent && ent.id === collection.id)
if (indexOf >= 0) { if (indexOf >= 0) {
this.entities = this.entities.filter((ent) => ent.id !== collection.id) this.entities = this.entities.filter((ent) => ent.id !== collection.id)
this.totalEntities = this.entities.length this.totalEntities--
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities) this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
this.executeRebuild() this.executeRebuild()
} }
@@ -715,6 +720,7 @@ export default {
.bookshelfRow { .bookshelfRow {
background-image: var(--bookshelf-texture-img); background-image: var(--bookshelf-texture-img);
} }
.bookshelfDivider { .bookshelfDivider {
background: rgb(149, 119, 90); background: rgb(149, 119, 90);
background: var(--bookshelf-divider-bg); background: var(--bookshelf-divider-bg);
+8 -8
View File
@@ -9,7 +9,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg> </svg>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Home</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -17,7 +17,7 @@
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons">format_list_bulleted</span> <span class="material-icons">format_list_bulleted</span>
<p class="font-book pt-1" style="font-size: 0.9rem">Latest</p> <p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -27,7 +27,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg> </svg>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Library</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -37,7 +37,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg> </svg>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Series</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -45,7 +45,7 @@
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons-outlined">collections_bookmark</span> <span class="material-icons-outlined">collections_bookmark</span>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Collections</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -58,7 +58,7 @@
/> />
</svg> </svg>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Authors</p> <p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -66,7 +66,7 @@
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="abs-icons icon-podcast text-xl"></span> <span class="abs-icons icon-podcast text-xl"></span>
<p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@@ -74,7 +74,7 @@
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
<span class="material-icons text-2xl">warning</span> <span class="material-icons text-2xl">warning</span>
<p class="font-book pt-1.5" style="font-size: 1rem">Issues</p> <p class="font-book pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center"> <div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
+3 -3
View File
@@ -15,7 +15,7 @@
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base"> <p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p> </p>
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">Unknown</p> <p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</p>
</div> </div>
<div class="text-gray-400 flex items-center"> <div class="text-gray-400 flex items-center">
@@ -159,8 +159,8 @@ export default {
return i.libraryItemId === libraryItemId return i.libraryItemId === libraryItemId
}) })
if (currentQueueIndex < 0) { if (currentQueueIndex < 0) {
console.error('Media finished not found in queue', this.playerQueueItems) console.error('Media finished not found in queue - using first in queue', this.playerQueueItems)
return currentQueueIndex = -1
} }
if (currentQueueIndex === this.playerQueueItems.length - 1) { if (currentQueueIndex === this.playerQueueItems.length - 1) {
console.log('Finished last item in queue') console.log('Finished last item in queue')
+11 -11
View File
@@ -15,41 +15,41 @@
<div class="flex my-2 -mx-2"> <div class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" label="Title" @input="titleUpdated" /> <ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" label="Author" /> <ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<div v-else class="w-full"> <div v-else class="w-full">
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
</div> </div>
</div> </div>
</div> </div>
<div v-if="!isPodcast" class="flex my-2 -mx-2"> <div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" label="Series" note="(optional)" /> <ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
</div> </div>
</div> </div>
</div> </div>
<tables-uploaded-files-table :files="item.itemFiles" title="Item Files" class="mt-8" /> <tables-uploaded-files-table :files="item.itemFiles" :title="$strings.HeaderItemFiles" class="mt-8" />
<tables-uploaded-files-table v-if="item.otherFiles.length" title="Other Files" :files="item.otherFiles" /> <tables-uploaded-files-table v-if="item.otherFiles.length" :title="$strings.HeaderOtherFiles" :files="item.otherFiles" />
<tables-uploaded-files-table v-if="item.ignoredFiles.length" title="Ignored Files" :files="item.ignoredFiles" /> <tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template> </template>
<widgets-alert v-if="uploadSuccess" type="success"> <widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">Successfully Uploaded!</p> <p class="text-base">{{ $strings.MessageUploaderItemSuccess }}</p>
</widgets-alert> </widgets-alert>
<widgets-alert v-if="uploadFailed" type="error"> <widgets-alert v-if="uploadFailed" type="error">
<p class="text-base">Failed to upload</p> <p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert> </widgets-alert>
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> <div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator text="Uploading..." /> <ui-loading-indicator :text="$strings.MessageUploading" />
</div> </div>
</div> </div>
</template> </template>
+110 -18
View File
@@ -14,7 +14,12 @@
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> </div>
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div> <div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }"> <div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
@@ -27,7 +32,9 @@
<!-- Placeholder Cover Title & Author --> <!-- Placeholder Cover Title & Author -->
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }"> <div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div> <div>
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p> <p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
{{ titleCleaned }}
</p>
</div> </div>
</div> </div>
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }"> <div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
@@ -236,6 +243,9 @@ export default {
// Only added to item object when collapseSeries is enabled // Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0 return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
}, },
seriesSequenceList() {
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
},
libraryItemIdsInSeries() { libraryItemIdsInSeries() {
// Only added to item object when collapseSeries is enabled // Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : [] return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
@@ -314,8 +324,18 @@ export default {
if (this.recentEpisode) return false // Dont show podcast error on episode card if (this.recentEpisode) return false // Dont show podcast error on episode card
return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid
}, },
libraryItemIdStreaming() {
return this.store.getters['getLibraryItemIdStreaming']
},
isStreaming() { isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId return this.libraryItemIdStreaming === this.libraryItemId
},
isQueued() {
const episodeId = this.recentEpisode ? this.recentEpisode.id : null
return this.store.getters['getIsMediaQueued'](this.libraryItemId, episodeId)
},
isStreamingFromDifferentLibrary() {
return this.store.getters['getIsStreamingFromDifferentLibrary']
}, },
showReadButton() { showReadButton() {
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader) return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
@@ -385,19 +405,32 @@ export default {
const items = [ const items = [
{ {
func: 'editPodcast', func: 'editPodcast',
text: 'Edit Podcast' text: this.$strings.ButtonEditPodcast
}, },
{ {
func: 'toggleFinished', func: 'toggleFinished',
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}` text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
} }
] ]
if (this.continueListeningShelf) { if (this.continueListeningShelf) {
items.push({ items.push({
func: 'removeFromContinueListening', func: 'removeFromContinueListening',
text: 'Remove from Continue Listening' text: this.$strings.ButtonRemoveFromContinueListening
}) })
} }
if (this.libraryItemIdStreaming && !this.isStreamingFromDifferentLibrary) {
if (!this.isQueued) {
items.push({
func: 'addToQueue',
text: this.$strings.ButtonQueueAddItem
})
} else if (!this.isStreaming) {
items.push({
func: 'removeFromQueue',
text: this.$strings.ButtonQueueRemoveItem
})
}
}
return items return items
} }
@@ -406,44 +439,59 @@ export default {
items = [ items = [
{ {
func: 'toggleFinished', func: 'toggleFinished',
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}` text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
} }
] ]
if (this.userCanUpdate) { if (this.userCanUpdate) {
items.push({ items.push({
func: 'openCollections', func: 'openCollections',
text: 'Add to Collection' text: this.$strings.LabelAddToCollection
}) })
} }
} }
if (this.userCanUpdate) { if (this.userCanUpdate) {
items.push({ items.push({
func: 'showEditModalFiles', func: 'showEditModalFiles',
text: 'Files' text: this.$strings.HeaderFiles
}) })
items.push({ items.push({
func: 'showEditModalMatch', func: 'showEditModalMatch',
text: 'Match' text: this.$strings.HeaderMatch
}) })
} }
if (this.userIsAdminOrUp && !this.isFile) { if (this.userIsAdminOrUp && !this.isFile) {
items.push({ items.push({
func: 'rescan', func: 'rescan',
text: 'Re-Scan' text: this.$strings.ButtonReScan
}) })
} }
if (this.series && this.bookMount) { if (this.series && this.bookMount) {
items.push({ items.push({
func: 'removeSeriesFromContinueListening', func: 'removeSeriesFromContinueListening',
text: 'Remove Series from Continue Series' text: this.$strings.ButtonRemoveSeriesFromContinueSeries
}) })
} }
if (this.continueListeningShelf) { if (this.continueListeningShelf) {
items.push({ items.push({
func: 'removeFromContinueListening', func: 'removeFromContinueListening',
text: 'Remove from Continue Listening' text: this.$strings.ButtonRemoveFromContinueListening
}) })
} }
if (!this.isPodcast) {
if (this.libraryItemIdStreaming && !this.isStreamingFromDifferentLibrary) {
if (!this.isQueued) {
items.push({
func: 'addToQueue',
text: this.$strings.ButtonQueueAddItem
})
} else if (!this.isStreaming) {
items.push({
func: 'removeFromQueue',
text: this.$strings.ButtonQueueRemoveItem
})
}
}
}
return items return items
}, },
_socket() { _socket() {
@@ -515,7 +563,7 @@ export default {
} }
} }
var mediaMetadata = libraryItem.media.metadata var mediaMetadata = libraryItem.media.metadata
if (mediaMetadata.series) { if (mediaMetadata.series && Array.isArray(mediaMetadata.series)) {
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id) var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
if (newSeries) { if (newSeries) {
// update selected series // update selected series
@@ -533,7 +581,7 @@ export default {
if (this.isSelectionMode) { if (this.isSelectionMode) {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.selectBtnClick() this.selectBtnClick(e)
} else { } else {
var router = this.$router || this.$nuxt.$router var router = this.$router || this.$nuxt.$router
if (router) { if (router) {
@@ -577,12 +625,12 @@ export default {
.$patch(apiEndpoint, updatePayload) .$patch(apiEndpoint, updatePayload)
.then(() => { .then(() => {
this.processing = false this.processing = false
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.processing = false this.processing = false
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
}) })
}, },
editPodcast() { editPodcast() {
@@ -656,9 +704,40 @@ export default {
this.processing = false this.processing = false
}) })
}, },
addToQueue() {
var queueItem = {}
if (this.recentEpisode) {
queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: this.recentEpisode.id,
title: this.recentEpisode.title,
subtitle: this.mediaMetadata.title,
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
duration: this.recentEpisode.audioFile.duration || null,
coverPath: this.media.coverPath || null
}
} else {
queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: null,
title: this.title,
subtitle: this.author,
caption: '',
duration: this.media.duration || null,
coverPath: this.media.coverPath || null
}
}
this.store.commit('addItemToQueue', queueItem)
},
removeFromQueue() {
const episodeId = this.recentEpisode ? this.recentEpisode.id : null
this.store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId })
},
openCollections() { openCollections() {
this.store.commit('setSelectedLibraryItem', this.libraryItem) this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShowUserCollectionsModal', true) this.store.commit('globals/setShowCollectionsModal', true)
}, },
createMoreMenu() { createMoreMenu() {
if (!this.$refs.moreIcon) return if (!this.$refs.moreIcon) return
@@ -751,6 +830,7 @@ export default {
if (!podcastProgress || !podcastProgress.isFinished) { if (!podcastProgress || !podcastProgress.isFinished) {
queueItems.push({ queueItems.push({
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
@@ -762,6 +842,18 @@ export default {
} }
} }
} }
} else {
const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: null,
title: this.title,
subtitle: this.author,
caption: '',
duration: this.media.duration || null,
coverPath: this.media.coverPath || null
}
queueItems.push(queueItem)
} }
eventBus.$emit('play-item', { eventBus.$emit('play-item', {
@@ -5,14 +5,14 @@
<div class="w-full h-16 bg-primary"> <div class="w-full h-16 bg-primary">
<img v-if="image" :src="image" class="w-full h-full object-cover" /> <img v-if="image" :src="image" class="w-full h-full object-cover" />
</div> </div>
<p class="text-gray-400 text-xxs pt-1 text-center">{{ numEpisodes }} Episodes</p> <p class="text-gray-400 text-xxs pt-1 text-center">{{ numEpisodes }} {{ $strings.HeaderEpisodes }}</p>
</div> </div>
<div class="flex-grow pl-2" :style="{ maxWidth: detailsWidth + 'px' }"> <div class="flex-grow pl-2" :style="{ maxWidth: detailsWidth + 'px' }">
<p class="mb-1">{{ title }}</p> <p class="mb-1">{{ title }}</p>
<p class="text-xs mb-1 text-gray-300">{{ author }}</p> <p class="text-xs mb-1 text-gray-300">{{ author }}</p>
<p class="text-xs mb-2 text-gray-200">{{ description }}</p> <p class="text-xs mb-2 text-gray-200">{{ description }}</p>
<p class="text-xs truncate text-blue-200"> <p class="text-xs truncate text-blue-200">
Folder: <span class="font-mono">{{ folderPath }}</span> {{ $strings.LabelFolder }}: <span class="font-mono">{{ folderPath }}</span>
</p> </p>
</div> </div>
</div> </div>
+9 -9
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="sm:w-80 w-full relative"> <div class="sm:w-80 w-full relative">
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
@@ -10,16 +10,16 @@
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>Thinking...</p> <p>{{ $strings.MessageThinking }}</p>
</li> </li>
<li v-else-if="isFetching" class="py-2 px-2"> <li v-else-if="isFetching" class="py-2 px-2">
<p>Fetching...</p> <p>{{ $strings.MessageFetching }}</p>
</li> </li>
<li v-else-if="!totalResults" class="py-2 px-2"> <li v-else-if="!totalResults" class="py-2 px-2">
<p>No Results</p> <p>{{ $strings.MessageNoResults }}</p>
</li> </li>
<template v-else> <template v-else>
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p> <p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelBooks }}</p>
<template v-for="item in bookResults"> <template v-for="item in bookResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
@@ -28,7 +28,7 @@
</li> </li>
</template> </template>
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p> <p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelPodcasts }}</p>
<template v-for="item in podcastResults"> <template v-for="item in podcastResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
@@ -37,7 +37,7 @@
</li> </li>
</template> </template>
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p> <p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
<template v-for="item in authorResults"> <template v-for="item in authorResults">
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
@@ -46,7 +46,7 @@
</li> </li>
</template> </template>
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p> <p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelSeries }}</p>
<template v-for="item in seriesResults"> <template v-for="item in seriesResults">
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`"> <nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
@@ -55,7 +55,7 @@
</li> </li>
</template> </template>
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p> <p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
<template v-for="item in tagResults"> <template v-for="item in tagResults">
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
@@ -7,7 +7,7 @@
</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-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">
<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="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)">
<div class="flex items-center"> <div class="flex items-center">
@@ -30,12 +30,67 @@ export default {
}, },
data() { data() {
return { return {
showMenu: false, showMenu: false
bookItems: [ }
},
computed: {
selected: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selectedDesc: {
get() {
return this.descending
},
set(val) {
this.$emit('update:descending', val)
}
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
},
podcastItems() {
return [
{ {
text: 'Title', text: 'Title',
value: 'media.metadata.title' value: 'media.metadata.title'
}, },
{
text: 'Author',
value: 'media.metadata.author'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Size',
value: 'size'
},
{
text: '# of Episodes',
value: 'media.numTracks'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
}
]
},
bookItems() {
return [
{
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{ {
text: 'Author (First Last)', text: 'Author (First Last)',
value: 'media.metadata.authorName' value: 'media.metadata.authorName'
@@ -68,62 +123,33 @@ export default {
text: 'File Modified', text: 'File Modified',
value: 'mtimeMs' value: 'mtimeMs'
} }
], ]
podcastItems: [ },
seriesItems() {
return [
...this.bookItems,
{ {
text: 'Title', text: 'Sequence',
value: 'media.metadata.title' value: 'sequence'
},
{
text: 'Author',
value: 'media.metadata.author'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Size',
value: 'size'
},
{
text: '# of Episodes',
value: 'media.numTracks'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
} }
] ]
}
},
computed: {
selected: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selectedDesc: {
get() {
return this.descending
},
set(val) {
this.$emit('update:descending', val)
}
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
}, },
selectItems() { selectItems() {
if (this.isPodcast) return this.podcastItems let items = null
return this.bookItems if (this.isPodcast) {
items = this.podcastItems
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
items = this.seriesItems
} else {
items = this.bookItems
}
if (!items.some((i) => i.value === this.selected)) {
this.selected = items[0].value
this.selectedDesc = !this.defaultsToAsc(items[0].value)
}
return items
}, },
selectedText() { selectedText() {
var _selected = this.selected var _selected = this.selected
@@ -143,12 +169,13 @@ export default {
this.selectedDesc = !this.selectedDesc this.selectedDesc = !this.selectedDesc
} else { } else {
this.selected = val this.selected = val
if (val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF') { if (this.defaultsToAsc(val)) this.selectedDesc = false
this.selectedDesc = false
}
} }
this.showMenu = false this.showMenu = false
this.$nextTick(() => this.$emit('change', val)) this.$nextTick(() => this.$emit('change', val))
},
defaultsToAsc(val) {
return val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF' || val == 'sequence'
} }
} }
} }
+5 -2
View File
@@ -4,7 +4,7 @@
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> <div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div class="absolute cover-bg" ref="coverBg" /> <div class="absolute cover-bg" ref="coverBg" />
</div> </div>
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" /> <img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }"> <a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span> <span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
@@ -63,6 +63,9 @@ export default {
}, },
resolution() { resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px` return `${this.naturalWidth}x${this.naturalHeight}px`
},
placeholderUrl() {
return `${this.$config.routerBasePath}/book_placeholder.jpg`
} }
}, },
methods: { methods: {
@@ -72,7 +75,7 @@ export default {
} }
}, },
imageLoaded() { imageLoaded() {
if (this.$refs.cover) { if (this.$refs.cover && this.src !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover var { naturalWidth, naturalHeight } = this.$refs.cover
this.naturalHeight = naturalHeight this.naturalHeight = naturalHeight
this.naturalWidth = naturalWidth this.naturalWidth = naturalWidth
+35 -33
View File
@@ -10,28 +10,28 @@
<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">
<ui-text-input-with-label v-model="newUser.username" label="Username" /> <ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" /> <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
</div> </div>
</div> </div>
<div v-show="!isEditingRoot" class="flex py-2"> <div v-show="!isEditingRoot" class="flex py-2">
<div class="px-2 w-52"> <div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" /> <ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<div class="flex items-center pt-4 px-2"> <div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p> <p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" /> <ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
</div> </div>
</div> </div>
<div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4"> <div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4">
<p class="text-lg mb-2 font-semibold">Permissions</p> <p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Download</p> <p>{{ $strings.LabelPermissionsDownload }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.download" /> <ui-toggle-switch v-model="newUser.permissions.download" />
@@ -40,7 +40,7 @@
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Update</p> <p>{{ $strings.LabelPermissionsUpdate }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.update" /> <ui-toggle-switch v-model="newUser.permissions.update" />
@@ -49,7 +49,7 @@
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Delete</p> <p>{{ $strings.LabelPermissionsDelete }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.delete" /> <ui-toggle-switch v-model="newUser.permissions.delete" />
@@ -58,7 +58,7 @@
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Upload</p> <p>{{ $strings.LabelPermissionsUpload }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.upload" /> <ui-toggle-switch v-model="newUser.permissions.upload" />
@@ -67,7 +67,7 @@
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Access Explicit Content</p> <p>{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" /> <ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" />
@@ -76,7 +76,7 @@
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Access All Libraries</p> <p>{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" /> <ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
@@ -84,26 +84,26 @@
</div> </div>
<div v-if="!newUser.permissions.accessAllLibraries" class="my-4"> <div v-if="!newUser.permissions.accessAllLibraries" class="my-4">
<ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" label="Libraries Accessible to User" /> <ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" :label="$strings.LabelLibrariesAccessibleToUser" />
</div> </div>
<div class="flex items-cen~ter my-2 max-w-md"> <div class="flex items-cen~ter my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p>Can Access All Tags</p> <p>{{ $strings.LabelPermissionsAccessAllTags }}</p>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" /> <ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" />
</div> </div>
</div> </div>
<div v-if="!newUser.permissions.accessAllTags" class="my-4"> <div v-if="!newUser.permissions.accessAllTags" class="my-4">
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" label="Tags Accessible to User" /> <ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" />
</div> </div>
</div> </div>
<div class="flex pt-4 px-2"> <div class="flex pt-4 px-2">
<ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn> <ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">Submit</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -125,20 +125,6 @@ export default {
processing: false, processing: false,
newUser: {}, newUser: {},
isNew: true, isNew: true,
accountTypes: [
{
text: 'Guest',
value: 'guest'
},
{
text: 'User',
value: 'user'
},
{
text: 'Admin',
value: 'admin'
}
],
tags: [], tags: [],
loadingTags: false loadingTags: false
} }
@@ -161,11 +147,27 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
accountTypes() {
return [
{
text: this.$strings.LabelAccountTypeGuest,
value: 'guest'
},
{
text: this.$strings.LabelAccountTypeUser,
value: 'user'
},
{
text: this.$strings.LabelAccountTypeAdmin,
value: 'admin'
}
]
},
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
title() { title() {
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}` return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
}, },
isEditingRoot() { isEditingRoot() {
return this.account && this.account.type === 'root' return this.account && this.account.type === 'root'
@@ -249,7 +251,7 @@ export default {
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {
this.$toast.error(`Failed to update account: ${data.error}`) this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
} else { } else {
console.log('Account updated', data.user) console.log('Account updated', data.user)
@@ -258,7 +260,7 @@ export default {
this.$store.commit('user/setUserToken', data.user.token) this.$store.commit('user/setUserToken', data.user.token)
} }
this.$toast.success('Account updated') this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
this.show = false this.show = false
} }
}) })
@@ -2,14 +2,14 @@
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing"> <modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
<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="font-book text-3xl text-white truncate">Set Backup Schedule</p> <p class="font-book text-3xl text-white truncate">{{ $strings.HeaderSetBackupSchedule }}</p>
</div> </div>
</template> </template>
<div v-if="show && newCronExpression" 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 v-if="show && newCronExpression" 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">
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" /> <widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? 'Save Backup Schedule' : 'No update necessary' }}</ui-btn> <ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -7,40 +7,36 @@
</template> </template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full"> <div v-if="show" class="w-full h-full py-4">
<div class="py-4 px-4">
<h1 class="text-2xl">Quick Match {{ selectedBookIds.length }} Books</h1>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96"> <div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
<div class="flex px-8 items-center py-2"> <div class="flex px-8 items-center py-2">
<p class="pr-4">Provider</p> <p class="pr-4">{{ $strings.LabelProvider }}</p>
<ui-dropdown v-model="options.provider" :items="providers" small /> <ui-dropdown v-model="options.provider" :items="providers" small />
</div> </div>
<p class="text-base px-8 py-2">Quick Match will attempt to add missing covers and metadata for the selected books. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.</p> <p class="text-base px-8 py-2">{{ $strings.MessageBatchQuickMatchDescription }}</p>
<div class="flex px-8 items-end py-2"> <div class="flex px-8 items-end py-2">
<ui-toggle-switch v-model="options.overrideCover"/> <ui-toggle-switch v-model="options.overrideCover" />
<ui-tooltip :text="tooltips.updateCovers"> <ui-tooltip :text="$strings.LabelUpdateCoverHelp">
<p class="pl-4"> <p class="pl-4">
Update Covers {{ $strings.LabelUpdateCover }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex px-8 items-end py-2"> <div class="flex px-8 items-end py-2">
<ui-toggle-switch v-model="options.overrideDetails"/> <ui-toggle-switch v-model="options.overrideDetails" />
<ui-tooltip :text="tooltips.updateDetails"> <ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
<p class="pl-4"> <p class="pl-4">
Update Details {{ $strings.LabelUpdateDetails }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="mt-4 py-4 border-b border-white border-opacity-10 text-white text-opacity-80 border-t border-white border-opacity-5"> <div class="mt-4 pt-4 text-white text-opacity-80 border-t border-white border-opacity-5">
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<ui-btn type="button" @click="show = false">Cancel</ui-btn> <ui-btn type="button" @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" @click="doBatchQuickMatch">Continue</ui-btn> <ui-btn color="success" @click="doBatchQuickMatch">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -60,10 +56,6 @@ export default {
overrideDetails: true, overrideDetails: true,
overrideCover: true, overrideCover: true,
overrideDefaults: true overrideDefaults: true
},
tooltips: {
updateCovers: 'Allow overwriting of existing covers for the selected books when a match is located.',
updateDetails: 'Allow overwriting of existing details for the selected books when a match is located.'
} }
} }
}, },
@@ -84,7 +76,7 @@ export default {
} }
}, },
title() { title() {
return `${this.selectedBookIds.length} Items Selected` return this.$getString('MessageItemsSelected', [this.selectedBookIds.length])
}, },
showBatchQuickMatchModal() { showBatchQuickMatchModal() {
return this.$store.state.globals.showBatchQuickMatchModal return this.$store.state.globals.showBatchQuickMatchModal
@@ -107,7 +99,7 @@ export default {
init() { init() {
// If we don't have a set provider (first open of dialog) or we've switched library, set // If we don't have a set provider (first open of dialog) or we've switched library, set
// the selected provider to the current library default provider // the selected provider to the current library default provider
if (!this.options.provider || (this.options.lastUsedLibrary != this.currentLibraryId)) { if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
this.options.lastUsedLibrary = this.currentLibraryId this.options.lastUsedLibrary = this.currentLibraryId
this.options.provider = this.libraryProvider this.options.provider = this.libraryProvider
} }
@@ -125,10 +117,12 @@ export default {
}) })
.then(() => { .then(() => {
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!') this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!')
}).catch((error) => { })
.catch((error) => {
this.$toast.error('Batch quick match failed') this.$toast.error('Batch quick match failed')
console.error('Failed to batch quick match', error) console.error('Failed to batch quick match', error)
}).finally(() => { })
.finally(() => {
this.processing = false this.processing = false
this.$store.commit('setProcessingBatch', false) this.$store.commit('setProcessingBatch', false)
this.show = false this.show = false
+8 -8
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'"> <modals-modal v-model="show" name="bookmarks" :width="500" :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="font-book text-3xl text-white truncate">Your Bookmarks</p> <p class="font-book text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
</div> </div>
</template> </template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
@@ -11,7 +11,7 @@
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" /> <modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
</template> </template>
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center"> <div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">No Bookmarks</p> <p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div> </div>
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" /> <div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
<form v-if="!hideCreate" @submit.prevent="submitCreateBookmark"> <form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
@@ -85,10 +85,10 @@ export default {
this.$axios this.$axios
.$delete(`/api/me/item/${this.libraryItemId}/bookmark/${bm.time}`) .$delete(`/api/me/item/${this.libraryItemId}/bookmark/${bm.time}`)
.then(() => { .then(() => {
this.$toast.success('Bookmark removed') this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(`Failed to remove bookmark`) this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
console.error(error) console.error(error)
}) })
this.show = false this.show = false
@@ -101,10 +101,10 @@ export default {
this.$axios this.$axios
.$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark) .$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => { .then(() => {
this.$toast.success('Bookmark updated') this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(`Failed to update bookmark`) this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
console.error(error) console.error(error)
}) })
this.show = false this.show = false
@@ -120,10 +120,10 @@ export default {
this.$axios this.$axios
.$post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark) .$post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => { .then(() => {
this.$toast.success('Bookmark added') this.$toast.success(this.$strings.ToastBookmarkCreateSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(`Failed to create bookmark`) this.$toast.error(this.$strings.ToastBookmarkCreateFailed)
console.error(error) console.error(error)
}) })
@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing"> <modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
<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="font-book text-3xl text-white truncate">Collection</p> <p class="font-book text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
</div> </div>
</template> </template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh"> <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">
@@ -14,15 +14,15 @@
<!-- <ui-btn type="button" @click="showImageUploader = true">Upload</ui-btn> --> <!-- <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="Name" class="mb-2" /> <ui-text-input-with-label v-model="newCollectionName" :label="$strings.LabelName" class="mb-2" />
<ui-textarea-with-label v-model="newCollectionDescription" label="Description" /> <ui-textarea-with-label v-model="newCollectionDescription" :label="$strings.LabelDescription" />
</div> </div>
</div> </div>
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex"> <div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">Remove</ui-btn> <ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">Save</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</form> </form>
</template> </template>
@@ -96,20 +96,19 @@ export default {
this.newCollectionDescription = this.collection.description || '' this.newCollectionDescription = this.collection.description || ''
}, },
removeClick() { removeClick() {
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) { if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
this.processing = true this.processing = true
var collectionName = this.collectionName
this.$axios this.$axios
.$delete(`/api/collections/${this.collection.id}`) .$delete(`/api/collections/${this.collection.id}`)
.then(() => { .then(() => {
this.processing = false this.processing = false
this.show = false this.show = false
this.$toast.success(`Collection "${collectionName}" Removed`) this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove collection', error) console.error('Failed to remove collection', error)
this.processing = false this.processing = false
this.$toast.error(`Failed to remove collection`) this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
}) })
} }
}, },
@@ -133,12 +132,12 @@ export default {
console.log('Collection Updated', collection) console.log('Collection Updated', collection)
this.processing = false this.processing = false
this.show = false this.show = false
this.$toast.success(`Collection "${collection.name}" Updated`) this.$toast.success(this.$strings.ToastCollectionUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update collection', error) console.error('Failed to update collection', error)
this.processing = false this.processing = false
this.$toast.error(`Failed to update collection`) this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
}) })
} }
}, },
@@ -8,14 +8,14 @@
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop> <div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
<div class="flex"> <div class="flex">
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80"> <div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" label="Series Name" /> <ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" />
</div> </div>
<div class="w-24 sm:w-28 md:w-40 p-1"> <div class="w-24 sm:w-28 md:w-40 p-1">
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" label="Sequence" /> <ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
</div> </div>
</div> </div>
<div class="flex justify-end mt-2 p-1"> <div class="flex justify-end mt-2 p-1">
<ui-btn type="submit">Save</ui-btn> <ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div> </div>
</form> </form>
@@ -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="font-book text-3xl text-white truncate">Session {{ _session.id }}</p> <p class="font-book text-3xl 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">
@@ -15,90 +15,90 @@
<div class="flex flex-wrap mb-4"> <div class="flex flex-wrap mb-4">
<div class="w-full md:w-2/3"> <div class="w-full md:w-2/3">
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2">Details</p> <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2">{{ $strings.HeaderDetails }}</p>
<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">Started At</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
<div class="px-1"> <div class="px-1">
{{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }} {{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }}
</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">Updated At</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
<div class="px-1"> <div class="px-1">
{{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }} {{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }}
</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">Listened for</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelTimeListened }}</div>
<div class="px-1"> <div class="px-1">
{{ $elapsedPrettyExtended(_session.timeListening) }} {{ $elapsedPrettyExtended(_session.timeListening) }}
</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">Start Time</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartTime }}</div>
<div class="px-1"> <div class="px-1">
{{ $secondsToTimestamp(_session.startTime) }} {{ $secondsToTimestamp(_session.startTime) }}
</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">Last Time</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelLastTime }}</div>
<div class="px-1"> <div class="px-1">
{{ $secondsToTimestamp(_session.currentTime) }} {{ $secondsToTimestamp(_session.currentTime) }}
</div> </div>
</div> </div>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Item</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">Library Id</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelLibrary }} Id</div>
<div class="px-1"> <div class="px-1">
{{ _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">Library Item Id</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelLibraryItem }} Id</div>
<div class="px-1"> <div class="px-1">
{{ _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">Episode Id</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelEpisode }} Id</div>
<div class="px-1"> <div class="px-1">
{{ _session.episodeId }} {{ _session.episodeId }}
</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">Media Type</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelMediaType }}</div>
<div class="px-1"> <div class="px-1">
{{ _session.mediaType }} {{ _session.mediaType }}
</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">Duration</div> <div class="w-40 px-1 text-gray-200">{{ $strings.LabelDuration }}</div>
<div class="px-1"> <div class="px-1">
{{ $elapsedPretty(_session.duration) }} {{ $elapsedPretty(_session.duration) }}
</div> </div>
</div> </div>
</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">User</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">{{ _session.userId }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Media Player</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>
<p class="mb-1">{{ _session.mediaPlayer }}</p> <p class="mb-1">{{ _session.mediaPlayer }}</p>
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p> <p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p>
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p> <p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p> <p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p> <p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p> <p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
<p v-if="deviceInfo.sdkVersion" class="mb-1">SDK Version: {{ deviceInfo.sdkVersion }}</p> <p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p>
<p v-if="deviceInfo.deviceType" class="mb-1">Type: {{ deviceInfo.deviceType }}</p> <p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p>
</div> </div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<ui-btn small color="error" @click.stop="deleteSessionClick">Delete</ui-btn> <ui-btn small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -156,7 +156,7 @@ export default {
methods: { methods: {
deleteSessionClick() { deleteSessionClick() {
const payload = { const payload = {
message: `Are you sure you want to delete this session?`, message: this.$strings.MessageConfirmDeleteSession,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.deleteSession() this.deleteSession()
@@ -172,7 +172,7 @@ export default {
.$delete(`/api/sessions/${this._session.id}`) .$delete(`/api/sessions/${this._session.id}`)
.then(() => { .then(() => {
this.processing = false this.processing = false
this.$toast.success('Session deleted successfully') this.$toast.success(this.$strings.ToastSessionDeleteSuccess)
this.$emit('removedSession') this.$emit('removedSession')
this.show = false this.show = false
}) })
@@ -180,7 +180,7 @@ export default {
this.processing = false this.processing = false
console.error('Failed to delete session', error) console.error('Failed to delete session', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || 'Failed to delete session') this.$toast.error(errMsg || this.$strings.ToastSessionDeleteFailed)
}) })
} }
}, },
+2 -2
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'"> <modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none"> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">Sleep Timer</p> <p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderSleepTimer }}</p>
</div> </div>
</template> </template>
@@ -32,7 +32,7 @@
<span class="pl-1 text-base font-mono">30m</span> <span class="pl-1 text-base font-mono">30m</span>
</ui-btn> </ui-btn>
</div> </div>
<ui-btn class="w-full" @click="$emit('cancel')">Cancel</ui-btn> <ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
+15 -15
View File
@@ -19,23 +19,23 @@
<div class="flex-grow"> <div class="flex-grow">
<div class="flex"> <div class="flex">
<div class="w-3/4 p-2"> <div class="w-3/4 p-2">
<ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" label="Name" /> <ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" :label="$strings.LabelName" />
</div> </div>
<div class="flex-grow p-2"> <div class="flex-grow p-2">
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" /> <ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div> </div>
</div> </div>
<div class="p-2"> <div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" label="Photo Path/URL" /> <ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
</div> </div>
<div class="p-2"> <div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" /> <ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div> </div>
<div class="flex pt-2 px-2"> <div class="flex pt-2 px-2">
<ui-btn type="button" @click="searchAuthor">Quick Match</ui-btn> <ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn type="submit">Submit</ui-btn> <ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -84,7 +84,7 @@ export default {
return this.author.id return this.author.id
}, },
title() { title() {
return 'Edit Author' return this.$strings.HeaderUpdateAuthor
} }
}, },
methods: { methods: {
@@ -103,23 +103,23 @@ export default {
} }
}) })
if (!Object.keys(updatePayload).length) { if (!Object.keys(updatePayload).length) {
this.$toast.info('No updates are necessary') this.$toast.info(this.$strings.MessageNoUpdateNecessary)
return return
} }
this.processing = true this.processing = true
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to update author') this.$toast.error(this.$strings.ToastAuthorUpdateFailed)
return null return null
}) })
if (result) { if (result) {
if (result.updated) { if (result.updated) {
this.$toast.success('Author updated') this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.show = false this.show = false
} else if (result.merged) { } else if (result.merged) {
this.$toast.success('Author merged') this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
this.show = false this.show = false
} else this.$toast.info('No updates were needed') } else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
} }
this.processing = false this.processing = false
}, },
@@ -131,11 +131,11 @@ export default {
this.processing = true this.processing = true
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to remove image') this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
return null return null
}) })
if (result && result.updated) { if (result && result.updated) {
this.$toast.success('Author image removed') this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
} }
this.processing = false this.processing = false
}, },
@@ -157,8 +157,8 @@ export default {
if (!response) { if (!response) {
this.$toast.error('Author not found') this.$toast.error('Author not found')
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) this.$toast.success('Author was updated') if (response.author.imagePath) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
else this.$toast.success('Author was updated (no image found)') else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
} else { } else {
this.$toast.info('No updates were made for Author') this.$toast.info('No updates were made for Author')
} }
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" name="collections" :processing="processing" :width="500" :height="'unset'"> <modals-modal v-model="show" name="collections" :processing="processing" :width="500" :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 pointer-events-none">
<p class="font-book text-3xl text-white truncate">{{ title }}</p> <p class="font-book text-3xl text-white truncate">{{ title }}</p>
</div> </div>
</template> </template>
@@ -9,8 +9,8 @@
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full"> <div v-if="show" class="w-full h-full">
<div class="py-4 px-4"> <div class="py-4 px-4">
<h1 v-if="!showBatchUserCollectionModal" class="text-2xl">Add to Collection</h1> <h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
<h1 v-else class="text-2xl">Add {{ selectedBookIds.length }} Books to Collection</h1> <h1 v-else class="text-2xl">{{ $getString('LabelAddToCollectionBatch', [selectedBookIds.length]) }}</h1>
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96"> <div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
<transition-group name="list-complete" tag="div"> <transition-group name="list-complete" tag="div">
@@ -20,15 +20,15 @@
</transition-group> </transition-group>
</div> </div>
<div v-if="!collections.length" class="flex h-32 items-center justify-center"> <div v-if="!collections.length" class="flex h-32 items-center justify-center">
<p class="text-xl">No Collections</p> <p class="text-xl">{{ $strings.MessageNoCollections }}</p>
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10" /> <div class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreateCollection"> <form @submit.prevent="submitCreateCollection">
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80"> <div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<ui-text-input v-model="newCollectionName" placeholder="New Collection" class="w-full" /> <ui-text-input v-model="newCollectionName" :placeholder="$strings.PlaceholderNewCollection" class="w-full" />
</div> </div>
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">Create</ui-btn> <ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
@@ -57,18 +57,21 @@ export default {
computed: { computed: {
show: { show: {
get() { get() {
return this.$store.state.globals.showUserCollectionsModal return this.$store.state.globals.showCollectionsModal
}, },
set(val) { set(val) {
this.$store.commit('globals/setShowUserCollectionsModal', val) this.$store.commit('globals/setShowCollectionsModal', val)
} }
}, },
title() { title() {
if (this.showBatchUserCollectionModal) { if (this.showBatchCollectionModal) {
return `${this.selectedBookIds.length} Items Selected` return this.$getString('MessageItemsSelected', [this.selectedBookIds.length])
} }
return this.selectedLibraryItem ? this.selectedLibraryItem.media.metadata.title : '' return this.selectedLibraryItem ? this.selectedLibraryItem.media.metadata.title : ''
}, },
collections() {
return this.$store.state.libraries.collections || []
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
@@ -78,14 +81,11 @@ export default {
selectedLibraryItemId() { selectedLibraryItemId() {
return this.selectedLibraryItem ? this.selectedLibraryItem.id : null return this.selectedLibraryItem ? this.selectedLibraryItem.id : null
}, },
collections() {
return this.$store.state.user.collections || []
},
sortedCollections() { sortedCollections() {
return this.collections return this.collections
.map((c) => { .map((c) => {
var includesBook = false var includesBook = false
if (this.showBatchUserCollectionModal) { if (this.showBatchCollectionModal) {
// Only show collection added if all books are in the collection // Only show collection added if all books are in the collection
var collectionBookIds = c.books.map((b) => b.id) var collectionBookIds = c.books.map((b) => b.id)
includesBook = !this.selectedBookIds.find((id) => !collectionBookIds.includes(id)) includesBook = !this.selectedBookIds.find((id) => !collectionBookIds.includes(id))
@@ -100,8 +100,8 @@ export default {
}) })
.sort((a, b) => (a.isBookIncluded ? -1 : 1)) .sort((a, b) => (a.isBookIncluded ? -1 : 1))
}, },
showBatchUserCollectionModal() { showBatchCollectionModal() {
return this.$store.state.globals.showBatchUserCollectionModal return this.$store.state.globals.showBatchCollectionModal
}, },
selectedBookIds() { selectedBookIds() {
return this.$store.state.selectedLibraryItems || [] return this.$store.state.selectedLibraryItems || []
@@ -112,24 +112,40 @@ export default {
}, },
methods: { methods: {
loadCollections() { loadCollections() {
this.$store.dispatch('user/loadUserCollections') if (!this.collections.length) {
this.processing = true
this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
.then((data) => {
if (data.results) {
this.$store.commit('libraries/setCollections', data.results || [])
}
})
.catch((error) => {
console.error('Failed to get collections', error)
this.$toast.error('Failed to load collections')
})
.finally(() => {
this.processing = false
})
}
}, },
removeFromCollection(collection) { removeFromCollection(collection) {
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
this.processing = true this.processing = true
if (this.showBatchUserCollectionModal) { if (this.showBatchCollectionModal) {
// BATCH Remove books // BATCH Remove books
this.$axios this.$axios
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Books removed from collection`, updatedCollection) console.log(`Books removed from collection`, updatedCollection)
this.$toast.success('Books removed from collection') this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove books from collection', error) console.error('Failed to remove books from collection', error)
this.$toast.error('Failed to remove books from collection') this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
this.processing = false this.processing = false
}) })
} else { } else {
@@ -138,12 +154,12 @@ export default {
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`) .$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection) console.log(`Book removed from collection`, updatedCollection)
this.$toast.success('Book removed from collection') this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove book from collection', error) console.error('Failed to remove book from collection', error)
this.$toast.error('Failed to remove book from collection') this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
this.processing = false this.processing = false
}) })
} }
@@ -152,7 +168,7 @@ export default {
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
this.processing = true this.processing = true
if (this.showBatchUserCollectionModal) { if (this.showBatchCollectionModal) {
// BATCH Remove books // BATCH Remove books
this.$axios this.$axios
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
@@ -189,7 +205,7 @@ export default {
} }
this.processing = true this.processing = true
var books = this.showBatchUserCollectionModal ? this.selectedBookIds : [this.selectedLibraryItemId] var books = this.showBatchCollectionModal ? this.selectedBookIds : [this.selectedLibraryItemId]
var newCollection = { var newCollection = {
books: books, books: books,
libraryId: this.currentLibraryId, libraryId: this.currentLibraryId,
+51 -49
View File
@@ -31,55 +31,7 @@ export default {
processing: false, processing: false,
libraryItem: null, libraryItem: null,
availableHeight: 0, availableHeight: 0,
marginTop: 0, marginTop: 0
tabs: [
{
id: 'details',
title: 'Details',
component: 'modals-item-tabs-details'
},
{
id: 'cover',
title: 'Cover',
component: 'modals-item-tabs-cover'
},
{
id: 'chapters',
title: 'Chapters',
component: 'modals-item-tabs-chapters',
mediaType: 'book'
},
{
id: 'episodes',
title: 'Episodes',
component: 'modals-item-tabs-episodes',
mediaType: 'podcast'
},
{
id: 'files',
title: 'Files',
component: 'modals-item-tabs-files'
},
{
id: 'match',
title: 'Match',
component: 'modals-item-tabs-match'
},
{
id: 'tools',
title: 'Tools',
component: 'modals-item-tabs-tools',
mediaType: 'book',
admin: true
},
{
id: 'schedule',
title: 'Schedule',
component: 'modals-item-tabs-schedule',
mediaType: 'podcast',
admin: true
}
]
} }
}, },
watch: { watch: {
@@ -122,6 +74,56 @@ export default {
this.$store.commit('setEditModalTab', val) this.$store.commit('setEditModalTab', val)
} }
}, },
tabs() {
return [
{
id: 'details',
title: this.$strings.HeaderDetails,
component: 'modals-item-tabs-details'
},
{
id: 'cover',
title: this.$strings.HeaderCover,
component: 'modals-item-tabs-cover'
},
{
id: 'chapters',
title: this.$strings.HeaderChapters,
component: 'modals-item-tabs-chapters',
mediaType: 'book'
},
{
id: 'episodes',
title: this.$strings.HeaderEpisodes,
component: 'modals-item-tabs-episodes',
mediaType: 'podcast'
},
{
id: 'files',
title: this.$strings.HeaderFiles,
component: 'modals-item-tabs-files'
},
{
id: 'match',
title: this.$strings.HeaderMatch,
component: 'modals-item-tabs-match'
},
{
id: 'tools',
title: this.$strings.HeaderTools,
component: 'modals-item-tabs-tools',
mediaType: 'book',
admin: true
},
{
id: 'schedule',
title: this.$strings.HeaderSchedule,
component: 'modals-item-tabs-schedule',
mediaType: 'podcast',
admin: true
}
]
},
showExperimentalFeatures() { showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures return this.$store.state.showExperimentalFeatures
}, },
@@ -3,8 +3,8 @@
<div class="w-full mb-4"> <div class="w-full mb-4">
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open /> <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open />
<div v-if="!chapters.length" class="py-4 text-center"> <div v-if="!chapters.length" class="py-4 text-center">
<p class="mb-8 text-xl">No Chapters</p> <p class="mb-8 text-xl">{{ $strings.MessageNoChapters }}</p>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">Add Chapters</ui-btn> <ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">{{ $strings.ButtonAddChapters }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
+18 -15
View File
@@ -14,11 +14,14 @@
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0"> <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
<div class="flex items-center"> <div class="flex items-center">
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected"><span class="hidden md:inline-block">Upload Cover</span><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input> <ui-file-input ref="fileInput" @change="fileUploadSelected"
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input
>
</div> </div>
<form @submit.prevent="submitForm" class="flex flex-grow"> <form @submit.prevent="submitForm" class="flex flex-grow">
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" /> <ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">Update</ui-btn> <ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">{{ $strings.ButtonSave }}</ui-btn>
</form> </form>
</div> </div>
@@ -26,7 +29,7 @@
<div class="flex items-center justify-center py-2"> <div class="flex items-center justify-center py-2">
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p> <p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? 'Hide' : 'Show' }}</ui-btn> <ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
</div> </div>
<div v-if="showLocalCovers" class="flex items-center justify-center"> <div v-if="showLocalCovers" class="flex items-center justify-center">
@@ -44,19 +47,19 @@
<form @submit.prevent="submitSearchForm"> <form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 h-20"> <div class="flex items-center justify-start -mx-1 h-20">
<div class="w-40 px-1"> <div class="w-40 px-1">
<ui-dropdown v-model="provider" :items="providers" label="Provider" small /> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div> </div>
<div class="w-72 px-1"> <div class="w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" /> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div> </div>
<div v-show="provider != 'itunes'" class="w-72 px-1"> <div v-show="provider != 'itunes'" class="w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" label="Author" /> <ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div> </div>
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> <ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div> </div>
</form> </form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">No Covers Found</p> <p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
@@ -65,14 +68,14 @@
</div> </div>
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8"> <div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
<p class="text-lg">Preview Cover</p> <p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span> <span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div class="absolute bottom-0 right-0 flex py-4 px-5"> <div class="absolute bottom-0 right-0 flex py-4 px-5">
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">Clear</ui-btn> <ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">Upload</ui-btn> <ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">{{ $strings.ButtonUpload }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -125,9 +128,9 @@ export default {
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
}, },
searchTitleLabel() { searchTitleLabel() {
if (this.provider.startsWith('audible')) return 'Search Title or ASIN' if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
else if (this.provider == 'itunes') return 'Search Term' else if (this.provider == 'itunes') return this.$strings.LabelSearchTerm
return 'Search Title' return this.$strings.LabelSearchTitle
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
+10 -8
View File
@@ -7,22 +7,24 @@
<div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'"> <div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn> <ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">{{ $strings.ButtonRemove }}</ui-btn>
<ui-icon-btn bg-color="error" icon="delete" class="md:hidden" :size="7" icon-font-size="1rem" @click.stop.prevent="removeItem" /> <ui-icon-btn bg-color="error" icon="delete" class="md:hidden" :size="7" icon-font-size="1rem" @click.stop.prevent="removeItem" />
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip :disabled="!!quickMatching" :text="`Populate empty ${mediaType} details & cover with first ${mediaType} result from '${libraryProvider}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.`" direction="bottom" class="mr-2 md:mr-4"> <ui-tooltip :disabled="!!quickMatching" :text="$getString('MessageQuickMatchDescription', [libraryProvider])" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn> <ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4"> <ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn> <ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-btn @click="save" class="mx-2 hidden md:block">Save</ui-btn> <!-- desktop -->
<ui-btn @click="save" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
<ui-btn @click="saveAndClose">Save<span class="hidden md:inline-block">&nbsp;& Close</span></ui-btn> <ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
<!-- mobile -->
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -177,7 +179,7 @@ export default {
this.$toast.success('Item details updated') this.$toast.success('Item details updated')
return true return true
} else { } else {
this.$toast.info('No updates were necessary') this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
} }
} }
return false return false
+10 -10
View File
@@ -2,29 +2,29 @@
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="w-full mb-4"> <div class="w-full mb-4">
<div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4"> <div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4">
<ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" /> <ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" :label="$strings.LabelLookForNewEpisodesAfterDate" class="max-w-xs mr-2" />
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" label="Max episodes" class="w-16 mr-2" input-class="h-10"> <ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
<div class="flex -mb-0.5"> <div class="flex -mb-0.5">
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">Limit</p> <p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited."> <ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
<span class="material-icons text-base">info_outlined</span> <span class="material-icons text-base">info_outlined</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
</ui-text-input-with-label> </ui-text-input-with-label>
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn> <ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">{{ $strings.ButtonCheckAndDownloadNewEpisodes }}</ui-btn>
</div> </div>
<div v-if="episodes.length" class="w-full p-4 bg-primary"> <div v-if="episodes.length" class="w-full p-4 bg-primary">
<p>Podcast Episodes</p> <p>{{ $strings.HeaderEpisodes }}</p>
</div> </div>
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">No Episodes</div> <div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
<table v-else class="text-sm tracksTable"> <table v-else class="text-sm tracksTable">
<tr class="font-book"> <tr class="font-book">
<th class="text-left">Sort #</th> <th class="text-left">Sort #</th>
<th class="text-left whitespace-nowrap">Episode #</th> <th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
<th class="text-left">Title</th> <th class="text-left">{{ $strings.EpisodeTitle }}</th>
<th class="text-center w-28">Duration</th> <th class="text-center w-28">{{ $strings.EpisodeDuration }}</th>
<th class="text-center w-28">Size</th> <th class="text-center w-28">{{ $strings.EpisodeSize }}</th>
</tr> </tr>
<tr v-for="episode in episodes" :key="episode.id"> <tr v-for="episode in episodes" :key="episode.id">
<td class="text-left"> <td class="text-left">
+45 -45
View File
@@ -3,22 +3,22 @@
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1"> <div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
<div class="w-36 px-1"> <div class="w-36 px-1">
<ui-dropdown v-model="provider" :items="providers" label="Provider" small /> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div> </div>
<div class="flex-grow md:w-72 px-1"> <div class="flex-grow md:w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" /> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div> </div>
<div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1"> <div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" label="Author" /> <ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div> </div>
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> <ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div> </div>
</form> </form>
<div v-show="processing" class="flex h-full items-center justify-center"> <div v-show="processing" class="flex h-full items-center justify-center">
<p>Loading...</p> <p>{{ $strings.MessageLoading }}</p>
</div> </div>
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center"> <div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
<p>No Results</p> <p>{{ $strings.MessageNoResults }}</p>
</div> </div>
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4"> <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
<template v-for="(res, index) in searchResults"> <template v-for="(res, index) in searchResults">
@@ -30,13 +30,13 @@
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch"> <div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
<span class="material-icons text-3xl">arrow_back</span> <span class="material-icons text-3xl">arrow_back</span>
</div> </div>
<p class="text-xl pl-3">Update Book Details</p> <p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div> </div>
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" /> <ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
<form @submit.prevent="submitMatchUpdate"> <form @submit.prevent="submitMatchUpdate">
<div v-if="selectedMatchOrig.cover" class="flex items-center py-2"> <div v-if="selectedMatchOrig.cover" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly label="Cover" class="flex-grow mx-4" /> <ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
<div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16"> <div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16">
<a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary"> <a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary">
<img :src="selectedMatch.cover" class="h-full w-full object-contain" /> <img :src="selectedMatch.cover" class="h-full w-full object-contain" />
@@ -46,50 +46,50 @@
<div v-if="selectedMatchOrig.title" class="flex items-center py-2"> <div v-if="selectedMatchOrig.title" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" /> <ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.title || '' }}</p> <p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.title || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2"> <div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" /> <ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.subtitle || '' }}</p> <p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.subtitle || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.author" class="flex items-center py-2"> <div v-if="selectedMatchOrig.author" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" /> <ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.authorName || '' }}</p> <p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.authorName || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2"> <div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" label="Narrator" /> <ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.narratorName || '' }}</p> <p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.description" class="flex items-center py-2"> <div v-if="selectedMatchOrig.description" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" /> <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p> <p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2"> <div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" /> <ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publisher || '' }}</p> <p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publisher || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2"> <div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" label="Published Year" /> <ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publishedYear || '' }}</p> <p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear || '' }}</p>
</div> </div>
</div> </div>
@@ -97,42 +97,42 @@
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" /> <widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.seriesName || '' }}</p> <p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.seriesName || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2"> <div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" label="Genres" /> <ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.genres.join(', ') }}</p> <p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2"> <div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" label="Tags" /> <ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ media.tags.join(', ') }}</p> <p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.language" class="flex items-center py-2"> <div v-if="selectedMatchOrig.language" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" label="Language" /> <ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.language || '' }}</p> <p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.language || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2"> <div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" /> <ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.isbn || '' }}</p> <p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.isbn || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2"> <div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" /> <ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.asin || '' }}</p> <p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.asin || '' }}</p>
</div> </div>
</div> </div>
@@ -140,33 +140,33 @@
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" /> <ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesId || '' }}</p> <p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesId || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2"> <div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" /> <ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.feedUrl || '' }}</p> <p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.feedUrl || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2"> <div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" /> <ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesPageUrl || '' }}</p> <p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesPageUrl || '' }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2"> <div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" label="Release Date" /> <ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.releaseDate || '' }}</p> <p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
</div> </div>
</div> </div>
<div class="flex items-center justify-end py-2"> <div class="flex items-center justify-end py-2">
<ui-btn color="success" type="submit">Update</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
@@ -258,9 +258,9 @@ export default {
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
}, },
searchTitleLabel() { searchTitleLabel() {
if (this.provider.startsWith('audible')) return 'Search Title or ASIN' if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
else if (this.provider == 'itunes') return 'Search Term' else if (this.provider == 'itunes') return this.$strings.LabelSearchTerm
return 'Search Title' return this.$strings.LabelSearchTitle
}, },
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem ? this.libraryItem.media || {} : {}
@@ -474,9 +474,9 @@ export default {
return false return false
}) })
if (success) { if (success) {
this.$toast.success('Item Cover Updated') this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
} else { } else {
this.$toast.error('Item Cover Failed to Update') this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
} }
console.log('Updated cover') console.log('Updated cover')
delete updatePayload.metadata.cover delete updatePayload.metadata.cover
@@ -490,14 +490,14 @@ export default {
}) })
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else { } else {
this.$toast.info('No detail updates were necessary') this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
} }
this.clearSelectedMatch() this.clearSelectedMatch()
this.$emit('selectTab', 'details') this.$emit('selectTab', 'details')
} else { } else {
this.$toast.error('Item Details Failed to Update') this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
} }
} else { } else {
this.clearSelectedMatch() this.clearSelectedMatch()
@@ -153,7 +153,7 @@ export default {
this.$toast.success('Item details updated') this.$toast.success('Item details updated')
return true return true
} else { } else {
this.$toast.info('No updates were necessary') this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
} }
} }
return false return false
+4 -4
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">Audiobook File Management Tools</p> <p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- Merge to m4b --> <!-- Merge to m4b -->
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8"> <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
@@ -12,7 +12,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<div> <div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center" <ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
>Open Manager >{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span> <span class="material-icons text-lg ml-2">launch</span>
</ui-btn> </ui-btn>
</div> </div>
@@ -43,14 +43,14 @@
<div class="flex-grow" /> <div class="flex-grow" />
<div> <div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center" <ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
>Open Manager >{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span> <span class="material-icons text-lg ml-2">launch</span>
</ui-btn> </ui-btn>
</div> </div>
</div> </div>
</div> </div>
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">No audio tracks</p> <p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
</div> </div>
</template> </template>
@@ -3,21 +3,21 @@
<div v-if="!showDirectoryPicker" class="w-full h-full py-4"> <div v-if="!showDirectoryPicker" class="w-full h-full py-4">
<div class="flex flex-wrap md:flex-nowrap -mx-1"> <div class="flex flex-wrap md:flex-nowrap -mx-1">
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0"> <div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="mediaType" :items="mediaTypes" label="Media Type" :disabled="!isNew" small @input="changedMediaType" /> <ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
</div> </div>
<div class="w-full md:flex-grow px-1 py-1 md:py-0"> <div class="w-full md:flex-grow px-1 py-1 md:py-0">
<ui-text-input-with-label v-model="name" label="Library Name" @blur="nameBlurred" /> <ui-text-input-with-label v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
</div> </div>
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0"> <div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
<ui-media-icon-picker v-model="icon" @input="iconChanged" /> <ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
</div> </div>
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0"> <div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small @input="formUpdated" /> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelMetadataProvider" small @input="formUpdated" />
</div> </div>
</div> </div>
<div class="w-full py-4"> <div class="w-full py-4">
<p class="px-1 text-sm font-semibold">Folders</p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2"> <div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" /> <ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
@@ -25,10 +25,10 @@
</div> </div>
<div class="flex py-1 px-2 items-center w-full"> <div class="flex py-1 px-2 items-center w-full">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" @blur="newFolderInputBlurred" /> <ui-editable-text v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
</div> </div>
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">Browse for Folder</ui-btn> <ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
</div> </div>
</div> </div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" /> <modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
@@ -53,20 +53,22 @@ export default {
folders: [], folders: [],
showDirectoryPicker: false, showDirectoryPicker: false,
newFolderPath: '', newFolderPath: '',
mediaType: null, mediaType: null
mediaTypes: [
{
value: 'book',
text: 'Books'
},
{
value: 'podcast',
text: 'Podcasts'
}
]
} }
}, },
computed: { computed: {
mediaTypes() {
return [
{
value: 'book',
text: this.$strings.LabelBooks
},
{
value: 'podcast',
text: this.$strings.LabelPodcasts
}
]
},
folderPaths() { folderPaths() {
return this.folders.map((f) => f.fullPath) return this.folders.map((f) => f.fullPath)
}, },
@@ -36,23 +36,6 @@ export default {
return { return {
processing: false, processing: false,
selectedTab: 'details', selectedTab: 'details',
tabs: [
{
id: 'details',
title: 'Details',
component: 'modals-libraries-edit-library'
},
{
id: 'settings',
title: 'Settings',
component: 'modals-libraries-library-settings'
},
{
id: 'schedule',
title: 'Schedule',
component: 'modals-libraries-schedule-scan'
}
],
libraryCopy: null libraryCopy: null
} }
}, },
@@ -66,10 +49,29 @@ export default {
} }
}, },
title() { title() {
return this.library ? 'Update Library' : 'New Library' return this.library ? this.$strings.HeaderUpdateLibrary : this.$strings.HeaderNewLibrary
}, },
buttonText() { buttonText() {
return this.library ? 'Update Library' : 'Create New Library' return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate
},
tabs() {
return [
{
id: 'details',
title: this.$strings.HeaderDetails,
component: 'modals-libraries-edit-library'
},
{
id: 'settings',
title: this.$strings.HeaderSettings,
component: 'modals-libraries-library-settings'
},
{
id: 'schedule',
title: this.$strings.HeaderSchedule,
component: 'modals-libraries-schedule-scan'
}
]
}, },
tabName() { tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab) var _tab = this.tabs.find((t) => t.id === this.selectedTab)
@@ -190,14 +192,14 @@ export default {
.then((res) => { .then((res) => {
this.processing = false this.processing = false
this.show = false this.show = false
this.$toast.success(`Library "${res.name}" updated successfully`) this.$toast.success(this.$getString('ToastLibraryUpdateSuccess', [res.name]))
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
} else { } else {
this.$toast.error('Failed to update library') this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
} }
this.processing = false this.processing = false
}) })
@@ -209,7 +211,7 @@ export default {
.then((res) => { .then((res) => {
this.processing = false this.processing = false
this.show = false this.show = false
this.$toast.success(`Library "${res.name}" created successfully`) this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
if (!this.$store.state.libraries.currentLibraryId) { if (!this.$store.state.libraries.currentLibraryId) {
console.log('Setting initially library id', res.id) console.log('Setting initially library id', res.id)
// First library added // First library added
@@ -221,7 +223,7 @@ export default {
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
} else { } else {
this.$toast.error('Failed to create library') this.$toast.error(this.$strings.ToastLibraryCreateFailed)
} }
this.processing = false this.processing = false
}) })
@@ -2,7 +2,7 @@
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10"> <div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
<div class="flex items-center py-1 mb-2"> <div class="flex items-center py-1 mb-2">
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span> <span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">Choose a Folder</p> <p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div> </div>
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> <div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p> <p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
@@ -27,16 +27,16 @@
</div> </div>
</div> </div>
<div v-else-if="loadingFolders" class="py-12 text-center"> <div v-else-if="loadingFolders" class="py-12 text-center">
<p>Loading folders...</p> <p>{{ $strings.MessageLoadingFolders }}</p>
</div> </div>
<div v-else class="py-12 text-center max-w-sm mx-auto"> <div v-else class="py-12 text-center max-w-sm mx-auto">
<p class="text-lg mb-2">No Folders Available</p> <p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p>
<p class="text-gray-300 mb-2">Note: folders already mapped will not be shown</p> <p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p>
<p v-if="isDebian" class="text-red-400">Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.</p> <p v-if="isDebian" class="text-red-400">{{ $strings.NoteFolderPickerDebian }}</p>
</div> </div>
<div class="w-full py-2"> <div class="w-full py-2">
<ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">Select Folder Path</ui-btn> <ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">{{ $strings.ButtonSelectFolderPath }}</ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -2,9 +2,9 @@
<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-2">
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" /> <ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
<ui-tooltip :text="tooltips.coverAspectRatio"> <ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Use square book covers {{ $strings.LabelSettingsSquareBookCovers }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -13,20 +13,20 @@
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" /> <ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
<ui-toggle-switch v-else disabled :value="false" /> <ui-toggle-switch v-else disabled :value="false" />
<p class="pl-4 text-base">Disable folder watcher for library</p> <p class="pl-4 text-base">{{ $strings.LabelSettingsDisableWatcherForLibrary }}</p>
</div> </div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</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="mediaType == 'book'" 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">Skip matching books that already have an ASIN</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="mediaType == 'book'" 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">Skip matching books that already have an ISBN</p> <p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -47,10 +47,7 @@ export default {
useSquareBookCovers: false, useSquareBookCovers: false,
disableWatcher: false, disableWatcher: false,
skipMatchingMediaWithAsin: false, skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false, skipMatchingMediaWithIsbn: false
tooltips: {
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
}
} }
}, },
computed: { computed: {
@@ -1,8 +1,8 @@
<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 justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<p class="text-base md:text-xl font-semibold">Schedule Automatic Library Scans</p> <p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleLibraryScans }}</p>
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" /> <ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
</div> </div>
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" /> <widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
</div> </div>
@@ -8,23 +8,25 @@
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300"> <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="w-full px-3 py-5 md:p-12">
<ui-dropdown v-model="newNotification.eventName" label="Notification Event" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" /> <ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
<ui-multi-select v-model="newNotification.urls" label="Apprise URL(s)" class="mb-2" /> <ui-multi-select v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
<ui-text-input-with-label v-model="newNotification.titleTemplate" label="Title Template" class="mb-2" /> <ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
<ui-textarea-with-label v-model="newNotification.bodyTemplate" label="Body Template" :rows="4" class="mb-2" /> <ui-textarea-with-label v-model="newNotification.bodyTemplate" :label="$strings.LabelNotificationBodyTemplate" :rows="4" class="mb-2" />
<p v-if="availableVariables" class="text-sm text-gray-300"><strong>Available variables:</strong> {{ availableVariables.join(', ') }}</p> <p v-if="availableVariables" class="text-sm text-gray-300">
<strong>{{ $strings.LabelNotificationAvailableVariables }}:</strong> {{ availableVariables.join(', ') }}
</p>
<div class="flex items-center pt-4"> <div class="flex items-center pt-4">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="newNotification.enabled" /> <ui-toggle-switch v-model="newNotification.enabled" />
<p class="text-lg pl-2">Enabled</p> <p class="text-lg pl-2">{{ $strings.LabelEnable }}</p>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">Submit</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -7,7 +7,7 @@
<p v-if="caption" class="text-gray-400 text-xs">{{ caption }}</p> <p v-if="caption" class="text-gray-400 text-xs">{{ caption }}</p>
</div> </div>
<div class="w-28"> <div class="w-28">
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">Streaming</p> <p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1"> <div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
<button class="outline-none mx-1 flex items-center" @click.stop="playClick"> <button class="outline-none mx-1 flex items-center" @click.stop="playClick">
<span class="material-icons text-success">play_arrow</span> <span class="material-icons text-success">play_arrow</span>
@@ -2,13 +2,13 @@
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'"> <modals-modal v-model="show" name="queue-items" :width="800" :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="font-book text-3xl text-white truncate">Player Queue</p> <p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlayerQueue }}</p>
</div> </div>
</template> </template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
<div v-if="show" class="w-full h-full"> <div v-if="show" class="w-full h-full">
<div class="pb-4 px-4 flex items-center"> <div class="pb-4 px-4 flex items-center">
<p class="text-base text-gray-200">Player Queue</p> <p class="text-base text-gray-200">{{ $strings.HeaderPlayerQueue }}</p>
<p class="text-base text-gray-400 px-4">{{ playerQueueItems.length }} Items</p> <p class="text-base text-gray-400 px-4">{{ playerQueueItems.length }} Items</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" /> <ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
@@ -59,11 +59,7 @@ export default {
this.show = false this.show = false
}, },
removeItem(item) { removeItem(item) {
const updatedQueue = this.playerQueueItems.filter((i) => { this.$store.commit('removeItemFromQueue', item)
if (!i.episodeId) return i.libraryItemId !== item.libraryItemId
return i.libraryItemId !== item.libraryItemId || i.episodeId !== item.episodeId
})
this.$store.commit('setPlayerQueueItems', updatedQueue)
} }
} }
} }
@@ -26,12 +26,12 @@ export default {
tabs: [ tabs: [
{ {
id: 'details', id: 'details',
title: 'Details', title: this.$strings.HeaderDetails,
component: 'modals-podcast-tabs-episode-details' component: 'modals-podcast-tabs-episode-details'
}, },
{ {
id: 'match', id: 'match',
title: 'Match', title: this.$strings.HeaderMatch,
component: 'modals-podcast-tabs-episode-match' component: 'modals-podcast-tabs-episode-match'
} }
] ]
+12 -12
View File
@@ -7,45 +7,45 @@
</template> </template>
<div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh"> <div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
<div class="w-full"> <div class="w-full">
<p class="text-lg font-semibold mb-2 px-2">Details</p> <p class="text-lg font-semibold mb-2 px-2">{{ $strings.HeaderDetails }}</p>
<div v-if="podcast.imageUrl" class="p-2 w-full"> <div v-if="podcast.imageUrl" class="p-2 w-full">
<img :src="podcast.imageUrl" class="h-16 w-16 object-contain" /> <img :src="podcast.imageUrl" class="h-16 w-16 object-contain" />
</div> </div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-text-input-with-label v-model="podcast.title" label="Title" @input="titleUpdated" /> <ui-text-input-with-label v-model="podcast.title" :label="$strings.LabelTitle" @input="titleUpdated" />
</div> </div>
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-text-input-with-label v-model="podcast.author" label="Author" /> <ui-text-input-with-label v-model="podcast.author" :label="$strings.LabelAuthor" />
</div> </div>
</div> </div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" readonly /> <ui-text-input-with-label v-model="podcast.feedUrl" :label="$strings.LabelFeedURL" readonly />
</div> </div>
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" label="Genres" /> <ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
</div> </div>
</div> </div>
<div class="p-2 w-full"> <div class="p-2 w-full">
<ui-textarea-with-label v-model="podcast.description" label="Description" :rows="3" /> <ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
</div> </div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" @input="folderUpdated" /> <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="$strings.LabelFolder" @input="folderUpdated" />
</div> </div>
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ui-text-input-with-label v-model="fullPath" label="Podcast Path" input-class="h-10" readonly /> <ui-text-input-with-label v-model="fullPath" :label="`${$strings.LabelPodcast} ${$strings.LabelPath}`" input-class="h-10" readonly />
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center py-4 px-2"> <div class="flex items-center py-4 px-2">
<div class="flex-grow" /> <div class="flex-grow" />
<div class="px-4"> <div class="px-4">
<ui-checkbox v-model="podcast.autoDownloadEpisodes" label="Auto Download Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm md:text-base font-semibold" /> <ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="$strings.LabelAutoDownloadEpisodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm md:text-base font-semibold" />
</div> </div>
<ui-btn color="success" @click="submit">Add Podcast</ui-btn> <ui-btn color="success" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -182,12 +182,12 @@ export default {
.$post('/api/podcasts', podcastPayload) .$post('/api/podcasts', podcastPayload)
.then((libraryItem) => { .then((libraryItem) => {
this.processing = false this.processing = false
this.$toast.success('Podcast created successfully') this.$toast.success(this.$strings.ToastPodcastCreateSuccess)
this.show = false this.show = false
this.$router.push(`/item/${libraryItem.id}`) this.$router.push(`/item/${libraryItem.id}`)
}) })
.catch((error) => { .catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to create podcast' var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', error) console.error('Failed to create podcast', error)
this.processing = false this.processing = false
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
@@ -9,14 +9,14 @@
<div class="w-full p-4"> <div class="w-full p-4">
<div class="flex items-center -mx-2 mb-2"> <div class="flex items-center -mx-2 mb-2">
<div class="w-full md:w-2/3 p-2"> <div class="w-full md:w-2/3 p-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" /> <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="$strings.LabelFolder" />
</div> </div>
<div class="w-full md:w-1/3 p-2 pt-6"> <div class="w-full md:w-1/3 p-2 pt-6">
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="text-sm font-semibold pl-2" /> <ui-checkbox v-model="autoDownloadEpisodes" :label="$strings.LabelAutoDownloadEpisodes" checkbox-bg="primary" border-color="gray-600" label-class="text-sm font-semibold pl-2" />
</div> </div>
</div> </div>
<p class="text-lg font-semibold mb-2">Podcasts to Add</p> <p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p>
<div class="w-full overflow-y-auto" style="max-height: 50vh"> <div class="w-full overflow-y-auto" style="max-height: 50vh">
<template v-for="(feed, index) in feedMetadata"> <template v-for="(feed, index) in feedMetadata">
@@ -26,7 +26,7 @@
</div> </div>
<div class="flex items-center py-4"> <div class="flex items-center py-4">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" @click="submit">Add Podcasts</ui-btn> <ui-btn color="success" @click="submit">{{ $strings.ButtonAddPodcasts }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -141,10 +141,10 @@ export default {
await this.$axios await this.$axios
.$post('/api/podcasts', podcastPayload) .$post('/api/podcasts', podcastPayload)
.then(() => { .then(() => {
this.$toast.success(`${podcastPayload.media.metadata.title}: Podcast created successfully`) this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`)
}) })
.catch((error) => { .catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to create podcast' var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', podcastPayload, error) console.error('Failed to create podcast', podcastPayload, error)
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`) this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
}) })
@@ -8,14 +8,13 @@
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden"> <div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div class="mb-4"> <div class="mb-4">
<p v-if="episode" class="text-lg text-gray-200 mb-4"> <p v-if="episode" class="text-lg text-gray-200 mb-4">
Are you sure you want to remove episode<br /><span class="text-base">{{ episodeTitle }}</span {{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}
>?
</p> </p>
<p v-else class="text-lg text-gray-200 mb-4">Are you sure you want to remove {{ episodes.length }} episodes?</p> <p v-else class="text-lg text-gray-200 mb-4">{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}</p>
<p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p> <p class="text-xs font-semibold text-warning text-opacity-90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
</div> </div>
<div class="flex justify-between items-center pt-4"> <div class="flex justify-between items-center pt-4">
<ui-checkbox v-model="hardDeleteFile" label="Hard delete file" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" /> <ui-checkbox v-model="hardDeleteFile" :label="$strings.LabelHardDeleteFile" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
<ui-btn @click="submit">{{ btnText }}</ui-btn> <ui-btn @click="submit">{{ btnText }}</ui-btn>
</div> </div>
@@ -61,12 +60,11 @@ export default {
return null return null
}, },
title() { title() {
if (this.episodes.length > 1) return `Remove ${this.episodes.length} episodes` if (this.episodes.length > 1) return this.$getString('HeaderRemoveEpisodes', [this.episodes.length])
return 'Remove Episode' return this.$strings.HeaderRemoveEpisode
}, },
btnText() { btnText() {
if (this.episodes.length > 1) return this.hardDeleteFile ? `Delete ${this.episodes.length} episodes` : `Remove ${this.episodes.length} episodes` return this.hardDeleteFile ? this.$strings.ButtonDelete : this.$strings.ButtonRemove
return this.hardDeleteFile ? 'Delete episode' : 'Remove episode'
}, },
episodeTitle() { episodeTitle() {
return this.episode ? this.episode.title : null return this.episode ? this.episode.title : null
@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing"> <modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
<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="font-book text-3xl text-white truncate">Episode</p> <p class="font-book text-3xl text-white truncate">{{ $strings.LabelEpisode }}</p>
</div> </div>
</template> </template>
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh"> <div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
@@ -17,7 +17,7 @@
</div> </div>
<p class="text-lg font-semibold mb-6">{{ title }}</p> <p class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" class="default-style" v-html="description" /> <div v-if="description" class="default-style" v-html="description" />
<p v-else class="mb-2">No description</p> <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@@ -2,29 +2,29 @@
<div> <div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-1/5 p-1"> <div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.season" label="Season" /> <ui-text-input-with-label v-model="newEpisode.season" :label="$strings.LabelSeason" />
</div> </div>
<div class="w-1/5 p-1"> <div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" /> <ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
</div> </div>
<div class="w-1/5 p-1"> <div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" /> <ui-text-input-with-label v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" />
</div> </div>
<div class="w-2/5 p-1"> <div class="w-2/5 p-1">
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" /> <ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
</div> </div>
<div class="w-full p-1"> <div class="w-full p-1">
<ui-text-input-with-label v-model="newEpisode.title" label="Title" /> <ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
</div> </div>
<div class="w-full p-1"> <div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" /> <ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
</div> </div>
<div class="w-full p-1 default-style"> <div class="w-full p-1 default-style">
<ui-rich-text-editor label="Description" v-model="newEpisode.description" /> <ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
</div> </div>
</div> </div>
<div class="flex items-center justify-end pt-4"> <div class="flex items-center justify-end pt-4">
<ui-btn @click="submit">Submit</ui-btn> <ui-btn @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
<div v-if="enclosureUrl" class="py-4"> <div v-if="enclosureUrl" class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p> <p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
@@ -2,18 +2,18 @@
<div style="min-height: 200px"> <div style="min-height: 200px">
<template v-if="!podcastFeedUrl"> <template v-if="!podcastFeedUrl">
<div class="py-8"> <div class="py-8">
<widgets-alert type="error">Podcast has no RSS feed url to use for matching</widgets-alert> <widgets-alert type="error">{{ $strings.MessagePodcastHasNoRSSFeedForMatching }}</widgets-alert>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div class="flex mb-2"> <div class="flex mb-2">
<ui-text-input-with-label v-model="episodeTitle" :disabled="isProcessing" label="Episode Title" class="pr-1" /> <ui-text-input-with-label v-model="episodeTitle" :disabled="isProcessing" :label="$strings.LabelEpisodeTitle" class="pr-1" />
<ui-btn class="mt-5 ml-1" :loading="isProcessing" type="submit">Search</ui-btn> <ui-btn class="mt-5 ml-1" :loading="isProcessing" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div> </div>
</form> </form>
<div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8"> <div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8">
<p class="text-center text-lg">No episode matches found</p> <p class="text-center text-lg">{{ $strings.MessageNoEpisodeMatchesFound }}</p>
</div> </div>
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)"> <div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p> <p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
+10 -10
View File
@@ -7,7 +7,7 @@
</template> </template>
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden"> <div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div v-if="currentFeedUrl" class="w-full"> <div v-if="currentFeedUrl" class="w-full">
<p class="text-lg font-semibold mb-4">Podcast RSS Feed is Open</p> <p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
<div class="w-full relative"> <div class="w-full relative">
<ui-text-input v-model="currentFeedUrl" readonly /> <ui-text-input v-model="currentFeedUrl" readonly />
@@ -16,20 +16,20 @@
</div> </div>
</div> </div>
<div v-else class="w-full"> <div v-else class="w-full">
<p class="text-lg font-semibold mb-4">Open RSS Feed</p> <p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
<div class="w-full relative mb-2"> <div class="w-full relative mb-2">
<ui-text-input-with-label v-model="newFeedSlug" label="RSS Feed Slug" /> <ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
<p class="text-xs text-gray-400 py-0.5 px-1">Feed will be {{ demoFeedUrl }}</p> <p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
</div> </div>
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">Warning: Most podcast apps will require the RSS feed URL is using HTTPS</p> <p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p> <p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
</div> </div>
<div v-show="userIsAdminOrUp" class="flex items-center pt-6"> <div v-show="userIsAdminOrUp" class="flex items-center pt-6">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn> <ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn> <ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -144,14 +144,14 @@ export default {
this.$axios this.$axios
.$post(`/api/items/${this.libraryItem.id}/close-feed`) .$post(`/api/items/${this.libraryItem.id}/close-feed`)
.then(() => { .then(() => {
this.$toast.success('RSS Feed Closed') this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
this.show = false this.show = false
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to close RSS feed', error) console.error('Failed to close RSS feed', error)
this.processing = false this.processing = false
this.$toast.error() this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
}) })
}, },
init() { init() {
+5 -5
View File
@@ -22,15 +22,15 @@
<span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span> <span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span>
</div> </div>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? 'Use full track' : 'Use chapter track'"> <button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2xl sm:text-3xl">queue_music</span>
</button>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack"> <div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span> <span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
</div> </div>
</ui-tooltip> </ui-tooltip>
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2xl sm:text-3xl">playlist_play</span>
</button>
</div> </div>
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" /> <player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
+3 -3
View File
@@ -5,10 +5,10 @@
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-lg mb-8 mt-2 px-1" v-html="message" /> <p class="text-lg mb-8 mt-2 px-1" v-html="message" />
<div class="flex px-1 items-center"> <div class="flex px-1 items-center">
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">Cancel</ui-btn> <ui-btn v-if="isYesNo" color="primary" @click="nevermind">{{ $strings.ButtonCancel }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="isYesNo" color="success" @click="confirm">Yes</ui-btn> <ui-btn v-if="isYesNo" color="success" @click="confirm">{{ $strings.ButtonYes }}</ui-btn>
<ui-btn v-else color="primary" @click="confirm">Ok</ui-btn> <ui-btn v-else color="primary" @click="confirm">{{ $strings.ButtonOk }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -1,6 +1,6 @@
<template> <template>
<div class="w-96 my-6 mx-auto"> <div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1> <h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
<div class="relative w-96 h-72"> <div class="relative w-96 h-72">
<div class="absolute top-0 left-0"> <div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels"> <template v-for="lbl in yAxisLabels">
@@ -34,24 +34,24 @@
</div> </div>
<div class="flex justify-between pt-12"> <div class="flex justify-between pt-12">
<div> <div>
<p class="text-sm text-center">Week Listening</p> <p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p> <p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-sm text-center">minutes</p> <p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div> </div>
<div> <div>
<p class="text-sm text-center">Daily Average</p> <p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p> <p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-sm text-center">minutes</p> <p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div> </div>
<div> <div>
<p class="text-sm text-center">Best Day</p> <p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p> <p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-sm text-center">minutes</p> <p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div> </div>
<div> <div>
<p class="text-sm text-center">Days</p> <p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p> <p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-sm text-center">in a row</p> <p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,7 +1,7 @@
<template> <template>
<div id="heatmap" class="w-full"> <div id="heatmap" class="w-full">
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)"> <div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
<p class="mb-2 px-1 text-sm text-gray-200">{{ Object.values(daysListening).length }} listening sessions in the last year</p> <p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageListeningSessionsInTheLastYear', [Object.values(daysListening).length]) }}</p>
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }"> <div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout"> <div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div> <div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
@@ -12,9 +12,9 @@
<div class="flex py-2 px-4" :style="{ marginTop: innerHeight + 'px' }"> <div class="flex py-2 px-4" :style="{ marginTop: innerHeight + 'px' }">
<div class="flex-grow" /> <div class="flex-grow" />
<p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">Less</p> <p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">{{ $strings.LabelLess }}</p>
<div v-for="block in legendBlocks" :key="block.id" :style="block.style" class="h-2.5 w-2.5 rounded-sm" style="margin-left: 1.5px; margin-right: 1.5px" /> <div v-for="block in legendBlocks" :key="block.id" :style="block.style" class="h-2.5 w-2.5 rounded-sm" style="margin-left: 1.5px; margin-right: 1.5px" />
<p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">More</p> <p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">{{ $strings.LabelMore }}</p>
</div> </div>
</div> </div>
</div> </div>
+5 -5
View File
@@ -6,7 +6,7 @@
</svg> </svg>
<div class="px-2"> <div class="px-2">
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items in Library</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
</div> </div>
</div> </div>
@@ -14,7 +14,7 @@
<span class="material-icons text-7xl">show_chart</span> <span class="material-icons text-7xl">show_chart</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Overall {{ useOverallHours ? 'Hours' : 'Days' }}</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsOverall }} {{ useOverallHours ? $strings.LabelStatsHours : $strings.LabelStatsDays }}</p>
</div> </div>
</div> </div>
@@ -24,7 +24,7 @@
</svg> </svg>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Authors</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
</div> </div>
</div> </div>
@@ -32,7 +32,7 @@
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span> <span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Size ({{ totalSizeMod }})</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
</div> </div>
</div> </div>
@@ -40,7 +40,7 @@
<span class="material-icons-outlined text-6xl pt-1">audio_file</span> <span class="material-icons-outlined text-6xl pt-1">audio_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p> <p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Audio Tracks</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1,71 +0,0 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
<p class="pr-4">Other Audio Files</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ files.length }}</span>
<div class="flex-grow" />
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-4">
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
</div>
<transition name="slide">
<div class="w-full" v-show="showTracks">
<table class="text-sm tracksTable">
<tr class="font-book">
<th class="text-left">Filename</th>
<th class="text-left">Size</th>
<th class="text-left">Duration</th>
<th class="text-left">Notes</th>
</tr>
<template v-for="track in files">
<tr :key="track.path">
<td class="font-book pl-2">
{{ track.filename }}
</td>
<td class="font-mono">
{{ $bytesPretty(track.size) }}
</td>
<td class="font-mono">
{{ $secondsToTimestamp(track.duration) }}
</td>
<td class="text-xs">
<p>{{ track.error || '' }}</p>
</td>
</tr>
</template>
</table>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
files: {
type: Array,
default: () => []
},
audiobookId: String
},
data() {
return {
showTracks: false
}
},
computed: {
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
}
},
methods: {
clickBar() {
this.showTracks = !this.showTracks
}
},
mounted() {}
}
</script>
+20 -22
View File
@@ -1,16 +1,16 @@
<template> <template>
<div class="text-center mt-4"> <div class="text-center mt-4">
<div class="flex py-4"> <div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">Upload Backup</ui-file-input> <ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn :loading="isBackingUp" @click="clickCreateBackup">Create Backup</ui-btn> <ui-btn :loading="isBackingUp" @click="clickCreateBackup">{{ $strings.ButtonCreateBackup }}</ui-btn>
</div> </div>
<div class="relative"> <div class="relative">
<table id="backups"> <table id="backups">
<tr> <tr>
<th>File</th> <th>{{ $strings.LabelFile }}</th>
<th class="hidden sm:table-cell w-32 md:w-56">Datetime</th> <th class="hidden sm:table-cell w-32 md:w-56">{{ $strings.LabelDatetime }}</th>
<th class="hidden sm:table-cell w-20 md:w-28">Size</th> <th class="hidden sm:table-cell w-20 md:w-28">{{ $strings.LabelSize }}</th>
<th class="w-36"></th> <th class="w-36"></th>
</tr> </tr>
<tr v-for="backup in backups" :key="backup.id" :class="!backup.serverVersion ? 'bg-error bg-opacity-10' : ''"> <tr v-for="backup in backups" :key="backup.id" :class="!backup.serverVersion ? 'bg-error bg-opacity-10' : ''">
@@ -21,7 +21,7 @@
<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)">Restore</ui-btn> <ui-btn v-if="backup.serverVersion" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a> <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">
@@ -33,7 +33,7 @@
</td> </td>
</tr> </tr>
<tr v-if="!backups.length" class="staticrow"> <tr v-if="!backups.length" class="staticrow">
<td colspan="4" class="text-lg">No Backups</td> <td colspan="4" class="text-lg">{{ $strings.MessageNoBackups }}</td>
</tr> </tr>
</table> </table>
<div v-show="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-25 flex items-center justify-center"> <div v-show="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-25 flex items-center justify-center">
@@ -43,16 +43,14 @@
<prompt-dialog v-model="showConfirmApply" :width="675"> <prompt-dialog v-model="showConfirmApply" :width="675">
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-error text-lg font-semibold">Important Notice!</p> <p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p> <p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.</p>
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p> <p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?</p>
<div class="flex px-1 items-center"> <div class="flex px-1 items-center">
<ui-btn color="primary" @click="showConfirmApply = false">Nevermind</ui-btn> <ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" @click="confirm">Apply Backup</ui-btn> <ui-btn color="success" @click="confirm">{{ $strings.ButtonRestore }}</ui-btn>
</div> </div>
</div> </div>
</prompt-dialog> </prompt-dialog>
@@ -90,23 +88,23 @@ export default {
.catch((error) => { .catch((error) => {
this.isBackingUp = false this.isBackingUp = false
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to apply backup') this.$toast.error(this.$strings.ToastBackupRestoreFailed)
}) })
}, },
deleteBackupClick(backup) { deleteBackupClick(backup) {
if (confirm(`Are you sure you want to delete backup for ${backup.datePretty}?`)) { if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) {
this.processing = true this.processing = true
this.$axios this.$axios
.$delete(`/api/backups/${backup.id}`) .$delete(`/api/backups/${backup.id}`)
.then((backups) => { .then((backups) => {
console.log('Backup deleted', backups) console.log('Backup deleted', backups)
this.$store.commit('setBackups', backups) this.$store.commit('setBackups', backups)
this.$toast.success(`Backup deleted`) this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
this.$toast.error('Failed to delete backup') this.$toast.error(this.$strings.ToastBackupDeleteFailed)
this.processing = false this.processing = false
}) })
} }
@@ -121,13 +119,13 @@ export default {
.$post('/api/backups') .$post('/api/backups')
.then((backups) => { .then((backups) => {
this.isBackingUp = false this.isBackingUp = false
this.$toast.success('Backup Successful') this.$toast.success(this.$strings.ToastBackupCreateSuccess)
this.$store.commit('setBackups', backups) this.$store.commit('setBackups', backups)
}) })
.catch((error) => { .catch((error) => {
this.isBackingUp = false this.isBackingUp = false
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Backup Failed') this.$toast.error(this.$strings.ToastBackupCreateFailed)
}) })
}, },
backupUploaded(file) { backupUploaded(file) {
@@ -141,12 +139,12 @@ export default {
.then((result) => { .then((result) => {
console.log('Upload backup result', result) console.log('Upload backup result', result)
this.$store.commit('setBackups', result) this.$store.commit('setBackups', result)
this.$toast.success('Backup upload success') this.$toast.success(this.$strings.ToastBackupUploadSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
var errorMessage = error.response && error.response.data ? error.response.data : 'Failed to upload backup' var errorMessage = error.response && error.response.data ? error.response.data : this.$strings.ToastBackupUploadFailed
this.$toast.error(errorMessage) this.$toast.error(errorMessage)
this.processing = false this.processing = false
}) })
+20 -7
View File
@@ -1,10 +1,10 @@
<template> <template>
<div class="w-full my-2"> <div class="w-full my-2">
<div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar"> <div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
<p class="pr-4">Chapters</p> <p class="pr-4">{{ $strings.HeaderChapters }}</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span> <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">Edit Chapters</ui-btn> <ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''"> <div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-icons text-4xl">expand_more</span>
</div> </div>
@@ -13,9 +13,9 @@
<table class="text-sm tracksTable" v-show="expanded || keepOpen"> <table class="text-sm tracksTable" v-show="expanded || keepOpen">
<tr class="font-book"> <tr class="font-book">
<th class="text-left w-16"><span class="px-4">Id</span></th> <th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">Title</th> <th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center">Start</th> <th class="text-center">{{ $strings.LabelStart }}</th>
<th class="text-center">End</th> <th class="text-center">{{ $strings.LabelEnd }}</th>
</tr> </tr>
<tr v-for="chapter in chapters" :key="chapter.id"> <tr v-for="chapter in chapters" :key="chapter.id">
<td class="text-left"> <td class="text-left">
@@ -72,11 +72,23 @@ export default {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
goToTimestamp(time) { goToTimestamp(time) {
const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryItem.libraryId,
episodeId: null,
title: this.metadata.title,
subtitle: this.metadata.authors.map((au) => au.name).join(', '),
caption: '',
duration: this.media.duration || null,
coverPath: this.media.coverPath || null
}
if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) { if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
episodeId: null, episodeId: null,
startTime: time startTime: time,
queueItems: [queueItem]
}) })
} else { } else {
const payload = { const payload = {
@@ -86,7 +98,8 @@ export default {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
episodeId: null, episodeId: null,
startTime: time startTime: time,
queueItems: [queueItem]
}) })
} }
}, },
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full bg-primary bg-opacity-40"> <div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary"> <div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
<p class="pr-4">Collection List</p> <p class="pr-4">{{ $strings.HeaderCollectionItems }}</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center"> <div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span> <span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
@@ -1,12 +1,12 @@
<template> <template>
<div class="w-full my-2"> <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"> <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">Library Files</p> <p class="pr-2 md:pr-4">{{ $strings.HeaderLibraryFiles }}</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"> <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">{{ files.length }}</span> <span class="text-sm font-mono">{{ files.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn> <ui-btn 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' : ''"> <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> <span class="material-icons text-4xl">expand_more</span>
</div> </div>
@@ -15,10 +15,10 @@
<div class="w-full" v-show="showFiles"> <div class="w-full" v-show="showFiles">
<table class="text-sm tracksTable"> <table class="text-sm tracksTable">
<tr class="font-book"> <tr class="font-book">
<th class="text-left px-4">Path</th> <th class="text-left px-4">{{ $strings.LabelPath }}</th>
<th class="text-left w-24 min-w-24">Size</th> <th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
<th class="text-left px-4 w-24">Filetype</th> <th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th> <th v-if="userCanDownload && !isMissing" class="text-center w-20">{{ $strings.LabelDownload }}</th>
</tr> </tr>
<template v-for="file in files"> <template v-for="file in files">
<tr :key="file.path"> <tr :key="file.path">
+6 -6
View File
@@ -7,9 +7,9 @@
</div> </div>
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> --> <!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn> <ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> <nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
<ui-btn small color="primary">Manage Tracks</ui-btn> <ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</nuxt-link> </nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-icons text-4xl">expand_more</span>
@@ -20,10 +20,10 @@
<table class="text-sm tracksTable"> <table class="text-sm tracksTable">
<tr class="font-book"> <tr class="font-book">
<th class="w-10">#</th> <th class="w-10">#</th>
<th class="text-left">Filename</th> <th class="text-left">{{ $strings.LabelFilename }}</th>
<th class="text-left w-20">Size</th> <th class="text-left w-20">{{ $strings.LabelSize }}</th>
<th class="text-left w-20">Duration</th> <th class="text-left w-20">{{ $strings.LabelDuration }}</th>
<th v-if="userCanDownload" class="text-center w-20">Download</th> <th v-if="userCanDownload" class="text-center w-20">{{ $strings.LabelDownload }}</th>
<th v-if="showExperimentalFeatures" class="text-center w-20"> <th v-if="showExperimentalFeatures" class="text-center w-20">
<div class="flex items-center"> <div class="flex items-center">
<p>Tone</p> <p>Tone</p>
@@ -12,9 +12,9 @@
<div class="w-full" v-show="expand"> <div class="w-full" v-show="expand">
<table class="text-sm tracksTable"> <table class="text-sm tracksTable">
<tr class="font-book"> <tr class="font-book">
<th class="text-left">Filename</th> <th class="text-left">{{ $strings.LabelFilename }}</th>
<th class="text-left">Size</th> <th class="text-left">{{ $strings.LabelSize }}</th>
<th class="text-left">Type</th> <th class="text-left">{{ $strings.LabelType }}</th>
</tr> </tr>
<template v-for="file in files"> <template v-for="file in files">
<tr :key="file.path"> <tr :key="file.path">
+10 -10
View File
@@ -1,20 +1,20 @@
<template> <template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">Users</h1> <h1 class="text-xl">{{ $strings.HeaderUsers }}</h1>
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser"> <div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser">
<span class="material-icons" style="font-size: 1.4rem">add</span> <span class="material-icons" style="font-size: 1.4rem">add</span>
</div> </div>
</div> </div>
<!-- <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> -->
<div class="text-center"> <div class="text-center">
<table id="accounts"> <table id="accounts">
<tr> <tr>
<th>Username</th> <th>{{ $strings.LabelUsername }}</th>
<th class="w-20">Type</th> <th class="w-20">{{ $strings.LabelAccountType }}</th>
<th class="hidden lg:table-cell">Activity</th> <th class="hidden lg:table-cell">{{ $strings.LabelActivity }}</th>
<th class="w-32 hidden sm:table-cell">Last Seen</th> <th class="w-32 hidden sm:table-cell">{{ $strings.LabelLastSeen }}</th>
<th class="w-32 hidden sm:table-cell">Created</th> <th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th> <th class="w-32"></th>
</tr> </tr>
<tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : 'bg-error bg-opacity-20'" @click="$router.push(`/config/users/${user.id}`)"> <tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : 'bg-error bg-opacity-20'" @click="$router.push(`/config/users/${user.id}`)">
@@ -88,7 +88,7 @@ export default {
methods: { methods: {
deleteUserClick(user) { deleteUserClick(user) {
if (this.isDeletingUser) return if (this.isDeletingUser) return
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) { if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
this.isDeletingUser = true this.isDeletingUser = true
this.$axios this.$axios
.$delete(`/api/users/${user.id}`) .$delete(`/api/users/${user.id}`)
@@ -97,12 +97,12 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('User deleted') this.$toast.success(this.$strings.ToastUserDeleteSuccess)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete user', error) console.error('Failed to delete user', error)
this.$toast.error('Failed to delete user') this.$toast.error(this.$strings.ToastUserDeleteFailed)
this.isDeletingUser = false this.isDeletingUser = false
}) })
} }
@@ -31,7 +31,7 @@
</div> </div>
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance"> <div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> <ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
</ui-tooltip> </ui-tooltip>
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'"> <div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
@@ -137,8 +137,22 @@ export default {
this.isHovering = false this.isHovering = false
}, },
playClick() { playClick() {
const queueItems = [
{
libraryItemId: this.book.id,
libraryId: this.book.libraryId,
episodeId: null,
title: this.bookTitle,
subtitle: this.bookAuthors.map((au) => au.name).join(', '),
caption: '',
duration: this.media.duration || null,
coverPath: this.media.coverPath || null
}
]
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.book.id libraryItemId: this.book.id,
queueItems
}) })
}, },
clickEdit() { clickEdit() {
@@ -153,12 +167,12 @@ 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(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
}) })
}, },
removeClick() { removeClick() {
@@ -168,12 +182,12 @@ export default {
.$delete(`/api/collections/${this.collectionId}/book/${this.book.id}`) .$delete(`/api/collections/${this.collectionId}/book/${this.book.id}`)
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection) console.log(`Book removed from collection`, updatedCollection)
this.$toast.success('Book removed from collection') this.$toast.success(this.$strings.ToastRemoveItemFromCollectionSuccess)
this.processingRemove = false this.processingRemove = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove book from collection', error) console.error('Failed to remove book from collection', error)
this.$toast.error('Failed to remove book from collection') this.$toast.error(this.$strings.ToastRemoveItemFromCollectionFailed)
this.processingRemove = false this.processingRemove = false
}) })
} }
@@ -1,7 +1,7 @@
<template> <template>
<div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">Libraries</h1> <h1 class="text-xl">{{ $strings.HeaderLibraries }}</h1>
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary"> <div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
<span class="material-icons" style="font-size: 1.4rem">add</span> <span class="material-icons" style="font-size: 1.4rem">add</span>
</div> </div>
@@ -14,12 +14,16 @@
</template> </template>
</draggable> </draggable>
<div v-if="!libraries.length" class="pb-4"> <div v-if="!libraries.length" class="pb-4">
<ui-btn @click="clickAddLibrary">Add your first library</ui-btn> <ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
</div> </div>
<p v-if="libraries.length" class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p> <p v-if="libraries.length" class="text-xs mt-4 text-gray-200">
*<strong>{{ $strings.ButtonForceReScan }}</strong> {{ $strings.MessageForceReScanDescription }}
</p>
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">**<strong>Match Books</strong> will attempt to match books in library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.</p> <p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
**<strong>{{ $strings.ButtonMatchBooks }}</strong> {{ $strings.MessageMatchBooksDescription }}
</p>
</div> </div>
</template> </template>
@@ -7,10 +7,10 @@
</svg> </svg>
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">Scan</ui-btn> <ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">{{ $strings.ButtonScan }}</ui-btn>
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">Force Re-Scan</ui-btn> <ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">{{ $strings.ButtonForceReScan }}</ui-btn>
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">Match Books</ui-btn> <ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">{{ $strings.ButtonMatchBooks }}</ui-btn>
<span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span> <span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
<span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span> <span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>
@@ -66,22 +66,22 @@ export default {
mobileMenuItems() { mobileMenuItems() {
const items = [ const items = [
{ {
text: 'Scan', text: this.$strings.ButtonScan,
value: 'scan' value: 'scan'
}, },
{ {
text: 'Force Re-Scan', text: this.$strings.ButtonForceReScan,
value: 'force-scan' value: 'force-scan'
} }
] ]
if (this.isBookLibrary) { if (this.isBookLibrary) {
items.push({ items.push({
text: 'Match Books', text: this.$strings.ButtonMatchBooks,
value: 'match-books' value: 'match-books'
}) })
} }
items.push({ items.push({
text: 'Delete', text: this.$strings.ButtonDelete,
value: 'delete' value: 'delete'
}) })
return items return items
@@ -122,28 +122,28 @@ export default {
this.$store this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id }) .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id })
.then(() => { .then(() => {
this.$toast.success('Library scan started') this.$toast.success(this.$strings.ToastLibraryScanStarted)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to start scan', error) console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan') this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
}) })
}, },
forceScan() { forceScan() {
if (confirm(`Force Re-Scan will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed to be used for the library item.\n\nAre you sure you want to force re-scan?`)) { if (confirm(this.$strings.MessageConfirmForceReScan)) {
this.$store this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 }) .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
.then(() => { .then(() => {
this.$toast.success('Library scan started') this.$toast.success(this.$strings.ToastLibraryScanStarted)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to start scan', error) console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan') this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
}) })
} }
}, },
deleteClick() { deleteClick() {
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) { if (confirm(this.$getString('MessageConfirmDeleteLibrary', [this.library.name]))) {
this.isDeleting = true this.isDeleting = true
this.$axios this.$axios
.$delete(`/api/libraries/${this.library.id}`) .$delete(`/api/libraries/${this.library.id}`)
@@ -152,12 +152,12 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('Library deleted') this.$toast.success(this.$strings.ToastLibraryDeleteSuccess)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete library', error) console.error('Failed to delete library', error)
this.$toast.error('Failed to delete library') this.$toast.error(this.$strings.ToastLibraryDeleteFailed)
this.isDeleting = false this.isDeleting = false
}) })
} }
@@ -20,7 +20,11 @@
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p> <p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</button> </button>
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'playlist_add' }}</span>
</button>
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
</ui-tooltip> </ui-tooltip>
@@ -83,9 +87,18 @@ export default {
duration() { duration() {
return this.$secondsToTimestamp(this.episode.duration) return this.$secondsToTimestamp(this.episode.duration)
}, },
libraryItemIdStreaming() {
return this.$store.getters['getLibraryItemIdStreaming']
},
isStreamingFromDifferentLibrary() {
return this.$store.getters['getIsStreamingFromDifferentLibrary']
},
isStreaming() { isStreaming() {
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id) return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
}, },
isQueued() {
return this.$store.getters['getIsMediaQueued'](this.libraryItemId, this.episode.id)
},
streamIsPlaying() { streamIsPlaying() {
return this.$store.state.streamIsPlaying && this.isStreaming return this.$store.state.streamIsPlaying && this.isStreaming
}, },
@@ -159,16 +172,25 @@ 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(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
}) })
}, },
removeClick() { removeClick() {
this.$emit('remove', this.episode) this.$emit('remove', this.episode)
},
queueBtnClick() {
if (this.isQueued) {
// Remove from queue
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episode.id })
} else {
// Add to queue
this.$emit('addToQueue', this.episode)
}
} }
} }
} }
@@ -1,23 +1,23 @@
<template> <template>
<div class="w-full py-6"> <div class="w-full py-6">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<p class="text-lg mb-0 font-semibold">Episodes</p> <p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<template v-if="isSelectionMode"> <template v-if="isSelectionMode">
<ui-tooltip :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom"> <ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" /> <ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
</ui-tooltip> </ui-tooltip>
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">Remove {{ selectedEpisodes.length }} episode{{ selectedEpisodes.length > 1 ? 's' : '' }}</ui-btn> <ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">{{ $getString('MessageRemoveEpisodes', [selectedEpisodes.length]) }}</ui-btn>
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">Cancel</ui-btn> <ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
</template> </template>
<template v-else> <template v-else>
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" /> <controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" />
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" /> <controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
</template> </template>
</div> </div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p> <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="episode in episodesSorted"> <template v-for="episode in episodesSorted">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" /> <tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" />
</template> </template>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" /> <modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
@@ -42,43 +42,7 @@ export default {
showPodcastRemoveModal: false, showPodcastRemoveModal: false,
selectedEpisodes: [], selectedEpisodes: [],
episodesToRemove: [], episodesToRemove: [],
processing: false, processing: false
sortItems: [
{
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
value: 'title'
},
{
text: 'Season',
value: 'season'
},
{
text: 'Episode',
value: 'episode'
}
],
filterItems: [
{
value: 'all',
text: 'Show All'
},
{
value: 'incomplete',
text: 'Incomplete'
},
{
value: 'complete',
text: 'Complete'
},
{
value: 'in_progress',
text: 'In Progress'
}
]
} }
}, },
watch: { watch: {
@@ -87,6 +51,46 @@ export default {
} }
}, },
computed: { computed: {
sortItems() {
return [
{
text: this.$strings.LabelPubDate,
value: 'publishedAt'
},
{
text: this.$strings.LabelTitle,
value: 'title'
},
{
text: this.$strings.LabelSeason,
value: 'season'
},
{
text: this.$strings.LabelEpisode,
value: 'episode'
}
]
},
filterItems() {
return [
{
value: 'all',
text: this.$strings.LabelShowAll
},
{
value: 'incomplete',
text: this.$strings.LabelIncomplete
},
{
value: 'complete',
text: this.$strings.LabelComplete
},
{
value: 'in_progress',
text: this.$strings.LabelInProgress
}
]
},
isSelectionMode() { isSelectionMode() {
return this.selectedEpisodes.length > 0 return this.selectedEpisodes.length > 0
}, },
@@ -127,6 +131,19 @@ export default {
} }
}, },
methods: { methods: {
addEpisodeToQueue(episode) {
const queueItem = {
libraryItemId: this.libraryItem.id,
libraryId: this.libraryItem.libraryId,
episodeId: episode.id,
title: episode.title,
subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null
}
this.$store.commit('addItemToQueue', queueItem)
},
toggleBatchFinished() { toggleBatchFinished() {
this.processing = true this.processing = true
var newIsFinished = !this.selectedIsFinished var newIsFinished = !this.selectedIsFinished
@@ -141,12 +158,12 @@ 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.processing = false this.processing = false
this.clearSelected() this.clearSelected()
}) })
.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.processing = false this.processing = false
}) })
@@ -185,6 +202,7 @@ export default {
if (!podcastProgress || !podcastProgress.isFinished) { if (!podcastProgress || !podcastProgress.isFinished) {
queueItems.push({ queueItems.push({
libraryItemId: this.libraryItem.id, libraryItemId: this.libraryItem.id,
libraryId: this.libraryItem.libraryId,
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
+1 -1
View File
@@ -28,7 +28,7 @@
</template> </template>
<li v-if="!itemsToShow.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> <li v-if="!itemsToShow.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal">No items</span> <span class="font-normal">{{ $strings.MessageNoItems }}</span>
</div> </div>
</li> </li>
</ul> </ul>
+1 -1
View File
@@ -24,7 +24,7 @@
</template> </template>
<li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> <li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal">No items</span> <span class="font-normal">{{ $strings.MessageNoItems }}</span>
</div> </div>
</li> </li>
</ul> </ul>
@@ -31,7 +31,7 @@
</template> </template>
<li v-if="!itemsToShow.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> <li v-if="!itemsToShow.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal">No items</span> <span class="font-normal">{{ $strings.MessageNoItems }}</span>
</div> </div>
</li> </li>
</ul> </ul>
+2 -2
View File
@@ -21,7 +21,7 @@
</template> </template>
<li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> <li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal">No items</span> <span class="font-normal">{{ $strings.MessageNoItems }}</span>
</div> </div>
</li> </li>
</ul> </ul>
@@ -74,7 +74,7 @@ export default {
if (this.searching) return if (this.searching) return
this.currentSearch = this.textInput this.currentSearch = this.textInput
this.searching = true this.searching = true
var results = await this.$axios.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15`).catch((error) => { var results = await this.$axios.$gest(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15`).catch((error) => {
console.error('Failed to get search results', error) console.error('Failed to get search results', error)
return [] return []
}) })
+3 -3
View File
@@ -2,21 +2,21 @@
<div class="w-full"> <div class="w-full">
<div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4"> <div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4">
<p class="text-sm mb-2"> <p class="text-sm mb-2">
Missing Parts <span class="text-sm">({{ missingParts.length }})</span> {{ $strings.LabelMissingParts }} <span class="text-sm">({{ missingParts.length }})</span>
</p> </p>
<p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p> <p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p>
</div> </div>
<div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4"> <div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4">
<p class="text-sm mb-2"> <p class="text-sm mb-2">
Invalid Parts <span class="text-sm">({{ invalidParts.length }})</span> {{ $strings.LabelInvalidParts }} <span class="text-sm">({{ invalidParts.length }})</span>
</p> </p>
<div> <div>
<p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p> <p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
</div> </div>
</div> </div>
<tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" /> <tables-tracks-table :title="$strings.LabelStatsAudioTracks" :tracks="media.tracks" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" />
</div> </div>
</template> </template>
+11 -11
View File
@@ -3,20 +3,20 @@
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex flex-wrap -mx-1"> <div class="flex flex-wrap -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" /> <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-3/4 px-1"> <div class="w-full md:w-3/4 px-1">
<!-- Authors filter only contains authors in this library, use query input to query all authors --> <!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" /> <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" endpoint="authors/search" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" /> <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" />
</div> </div>
</div> </div>
@@ -26,20 +26,20 @@
</div> </div>
</div> </div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" /> <ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" /> <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
@@ -51,14 +51,14 @@
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" /> <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
</div> </div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div> </div>
</div> </div>
</div> </div>
@@ -2,22 +2,22 @@
<div class="w-full py-2"> <div class="w-full py-2">
<div class="flex -mb-px"> <div class="flex -mb-px">
<div class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false"> <div class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
<p class="text-sm">Scheduler</p> <p class="text-sm">{{ $strings.HeaderSchedule }}</p>
</div> </div>
<div class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true"> <div class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
<p class="text-sm">Advanced</p> <p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
</div> </div>
</div> </div>
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 280px"> <div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 280px">
<template v-if="!showAdvancedView"> <template v-if="!showAdvancedView">
<ui-dropdown v-model="selectedInterval" @input="updateCron" label="Interval" :items="intervalOptions" class="mb-2" /> <ui-dropdown v-model="selectedInterval" @input="updateCron" :label="$strings.LabelInterval" :items="intervalOptions" class="mb-2" />
<ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" label="Weekdays to run" :items="weekdays" /> <ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" :label="$strings.LabelWeekdaysToRun" :items="weekdays" />
<div v-if="(selectedWeekdays.length && selectedInterval === 'custom') || selectedInterval === 'daily'" class="flex items-center py-2"> <div v-if="(selectedWeekdays.length && selectedInterval === 'custom') || selectedInterval === 'daily'" class="flex items-center py-2">
<ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" label="Hour" class="max-w-20" /> <ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" :label="$strings.LabelHour" class="max-w-20" />
<p class="text-xl px-2 mt-4">:</p> <p class="text-xl px-2 mt-4">:</p>
<ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" label="Minute" class="max-w-20" /> <ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" :label="$strings.LabelMinute" class="max-w-20" />
</div> </div>
<div v-if="description" class="w-full bg-primary bg-opacity-75 rounded-xl p-2 md:p-4 text-center mt-2"> <div v-if="description" class="w-full bg-primary bg-opacity-75 rounded-xl p-2 md:p-4 text-center mt-2">
@@ -25,15 +25,15 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<p class="px-1 text-sm font-semibold">Cron Expression</p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelCronExpression }}</p>
<ui-text-input ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" label="Cron Expression" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" /> <ui-text-input ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" />
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<widgets-loading-spinner v-if="isValidating" class="mr-2" /> <widgets-loading-spinner v-if="isValidating" class="mr-2" />
<span v-else class="material-icons-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span> <span v-else class="material-icons-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">Checking cron...</p> <p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p>
<p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p> <p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p>
<p v-else class="text-success text-base md:text-lg text-center">Valid cron expression</p> <p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
</div> </div>
</template> </template>
</div> </div>
@@ -137,31 +137,31 @@ export default {
weekdays() { weekdays() {
return [ return [
{ {
text: 'Sunday', text: this.$strings.WeekdaySunday,
value: 0 value: 0
}, },
{ {
text: 'Monday', text: this.$strings.WeekdayMonday,
value: 1 value: 1
}, },
{ {
text: 'Tuesday', text: this.$strings.WeekdayTuesday,
value: 2 value: 2
}, },
{ {
text: 'Wednesday', text: this.$strings.WeekdayWednesday,
value: 3 value: 3
}, },
{ {
text: 'Thursday', text: this.$strings.WeekdayThursday,
value: 4 value: 4
}, },
{ {
text: 'Friday', text: this.$strings.WeekdayFriday,
value: 5 value: 5
}, },
{ {
text: 'Saturday', text: this.$strings.WeekdaySaturday,
value: 6 value: 6
} }
] ]
+2 -1
View File
@@ -13,7 +13,7 @@
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled"> <div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }"> <div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items"> <template v-for="(item, index) in items">
<cards-lazy-book-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" /> <cards-lazy-book-card :key="item.id + '-' + shelfId" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" />
</template> </template>
</div> </div>
</div> </div>
@@ -35,6 +35,7 @@ export default {
type: Number, type: Number,
default: 1 default: 1
}, },
shelfId: String,
continueListeningShelf: Boolean continueListeningShelf: Boolean
}, },
data() { data() {
@@ -3,39 +3,39 @@
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1"> <div class="flex -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" label="Author" /> <ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" />
</div> </div>
</div> </div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" label="RSS Feed URL" class="mt-2" /> <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" label="Release Date" /> <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" /> <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
</div> </div>
<div class="flex-grow px-1 pt-6"> <div class="flex-grow px-1 pt-6">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div> </div>
</div> </div>
</div> </div>
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" label="Series" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" /> <ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" /> <modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" />
</div> </div>
+13 -6
View File
@@ -4,13 +4,13 @@
<app-side-rail v-if="isShowingSideRail" class="hidden md:block" /> <app-side-rail v-if="isShowingSideRail" class="hidden md:block" />
<div id="app-content" class="h-full" :class="{ 'has-siderail': isShowingSideRail }"> <div id="app-content" class="h-full" :class="{ 'has-siderail': isShowingSideRail }">
<Nuxt /> <Nuxt :key="currentLang" />
</div> </div>
<app-stream-container ref="streamContainer" /> <app-stream-container ref="streamContainer" />
<modals-item-edit-modal /> <modals-item-edit-modal />
<modals-user-collections-modal /> <modals-collections-add-create-modal />
<modals-edit-collection-modal /> <modals-edit-collection-modal />
<modals-podcast-edit-episode /> <modals-podcast-edit-episode />
<modals-podcast-view-episode /> <modals-podcast-view-episode />
@@ -31,7 +31,8 @@ export default {
socket: null, socket: null,
isSocketConnected: false, isSocketConnected: false,
isFirstSocketConnection: true, isFirstSocketConnection: true,
socketConnectionToastId: null socketConnectionToastId: null,
currentLang: null
} }
}, },
watch: { watch: {
@@ -297,10 +298,10 @@ export default {
this.$store.commit('user/updateMediaProgress', payload) this.$store.commit('user/updateMediaProgress', payload)
}, },
collectionAdded(collection) { collectionAdded(collection) {
this.$store.commit('user/addUpdateCollection', collection) this.$store.commit('libraries/addUpdateCollection', collection)
}, },
collectionUpdated(collection) { collectionUpdated(collection) {
this.$store.commit('user/addUpdateCollection', collection) this.$store.commit('libraries/addUpdateCollection', collection)
}, },
collectionRemoved(collection) { collectionRemoved(collection) {
if (this.$route.name.startsWith('collection')) { if (this.$route.name.startsWith('collection')) {
@@ -308,7 +309,7 @@ export default {
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`) this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`)
} }
} }
this.$store.commit('user/removeCollection', collection) this.$store.commit('libraries/removeCollection', collection)
}, },
rssFeedOpen(data) { rssFeedOpen(data) {
this.$store.commit('feeds/addFeed', data) this.$store.commit('feeds/addFeed', data)
@@ -519,6 +520,10 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to load tasks', error) console.error('Failed to load tasks', error)
}) })
},
changeLanguage(code) {
console.log('Changed lang', code)
this.currentLang = code
} }
}, },
beforeMount() { beforeMount() {
@@ -527,6 +532,7 @@ export default {
mounted() { mounted() {
this.updateBodyClass() this.updateBodyClass()
this.resize() this.resize()
this.$eventBus.$on('change-lang', this.changeLanguage)
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown) window.addEventListener('keydown', this.keyDown)
@@ -544,6 +550,7 @@ export default {
} }
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
window.removeEventListener('keydown', this.keyDown) window.removeEventListener('keydown', this.keyDown)
} }
+3 -10
View File
@@ -52,7 +52,8 @@ module.exports = {
'@/plugins/init.client.js', '@/plugins/init.client.js',
'@/plugins/axios.js', '@/plugins/axios.js',
'@/plugins/toast.js', '@/plugins/toast.js',
'@/plugins/utils.js' '@/plugins/utils.js',
'@/plugins/i18n.js'
], ],
// Auto import components: https://go.nuxtjs.dev/config-components // Auto import components: https://go.nuxtjs.dev/config-components
@@ -112,15 +113,7 @@ module.exports = {
icons: [ icons: [
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
sizes: "64x64" sizes: "any"
},
{
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
sizes: "192x192"
},
{
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
sizes: "512x512"
} }
] ]
}, },
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.2", "version": "2.2.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.2", "version": "2.2.4",
"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.2", "version": "2.2.4",
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+23 -13
View File
@@ -1,35 +1,39 @@
<template> <template>
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''"> <div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full max-w-xl mx-auto"> <div class="w-full max-w-xl mx-auto">
<h1 class="text-2xl">Account</h1> <h1 class="text-2xl">{{ $strings.HeaderAccount }}</h1>
<div class="my-4"> <div class="my-4">
<div class="flex -mx-2"> <div class="flex -mx-2">
<div class="w-2/3 px-2"> <div class="w-2/3 px-2">
<ui-text-input-with-label disabled :value="username" label="Username" /> <ui-text-input-with-label disabled :value="username" :label="$strings.LabelUsername" />
</div> </div>
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<ui-text-input-with-label disabled :value="usertype" label="Account Type" /> <ui-text-input-with-label disabled :value="usertype" :label="$strings.LabelAccountType" />
</div> </div>
</div> </div>
<div class="py-4">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguage }}</p>
<ui-dropdown v-model="selectedLanguage" :items="$languageCodeOptions" small class="max-w-48" @input="updateLocalLanguage" />
</div>
<div class="w-full h-px bg-primary my-4" /> <div class="w-full h-px bg-white/10 my-4" />
<p v-if="!isGuest" class="mb-4 text-lg">Change Password</p> <p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
<form v-if="!isGuest" @submit.prevent="submitChangePassword"> <form v-if="!isGuest" @submit.prevent="submitChangePassword">
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" /> <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" />
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" /> <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" />
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" /> <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" />
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p v-if="isRoot" class="text-error py-2 text-xs">* Root user is the only user that can have an empty password</p> <p v-if="isRoot" class="text-error py-2 text-xs">* {{ $strings.NoteChangeRootPassword }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-show="(password && newPassword && confirmPassword) || isRoot" type="submit" :loading="changingPassword" color="success">Submit</ui-btn> <ui-btn v-show="(password && newPassword && confirmPassword) || isRoot" type="submit" :loading="changingPassword" color="success">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
<div class="py-4 mt-8 flex"> <div class="py-4 mt-8 flex">
<ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-icons mr-4 icon-text">logout</span>Logout</ui-btn> <ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-icons mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -42,7 +46,8 @@ export default {
password: null, password: null,
newPassword: null, newPassword: null,
confirmPassword: null, confirmPassword: null,
changingPassword: false changingPassword: false,
selectedLanguage: ''
} }
}, },
computed: { computed: {
@@ -66,6 +71,9 @@ export default {
} }
}, },
methods: { methods: {
updateLocalLanguage(lang) {
this.$setLanguageCode(lang)
},
logout() { logout() {
var rootSocket = this.$root.socket || {} var rootSocket = this.$root.socket || {}
const logoutPayload = { const logoutPayload = {
@@ -113,6 +121,8 @@ export default {
}) })
} }
}, },
mounted() {} mounted() {
this.selectedLanguage = this.$languageCodes.current
}
} }
</script> </script>
+31 -31
View File
@@ -8,23 +8,23 @@
<span class="material-icons text-base">edit</span> <span class="material-icons text-base">edit</span>
</button> </button>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-base">Duration:</p> <p class="text-base">{{ $strings.LabelDuration }}:</p>
<p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p> <p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
</div> </div>
<div class="flex flex-wrap-reverse justify-center py-4"> <div class="flex flex-wrap-reverse justify-center py-4">
<div class="w-full max-w-3xl py-4"> <div class="w-full max-w-3xl py-4">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p> <p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" /> <ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
<div class="w-40" /> <div class="w-40" />
</div> </div>
<div class="flex items-center mb-3 py-1"> <div class="flex items-center mb-3 py-1">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">Shift Times</ui-btn> <ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn> <ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
<ui-btn color="success" small @click="saveChapters">Save</ui-btn> <ui-btn color="success" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
<div class="w-40" /> <div class="w-40" />
</div> </div>
@@ -34,13 +34,13 @@
<div class="w-12"></div> <div class="w-12"></div>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm mb-1 font-semibold pr-2">Time to shift in seconds</p> <p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
<ui-text-input v-model="shiftAmount" type="number" class="max-w-20" style="height: 30px" /> <ui-text-input v-model="shiftAmount" type="number" class="max-w-20" style="height: 30px" />
<ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">Add</ui-btn> <ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">{{ $strings.ButtonAdd }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<span class="material-icons text-gray-200 hover:text-white cursor-pointer" @click="showShiftTimes = false">close</span> <span class="material-icons text-gray-200 hover:text-white cursor-pointer" @click="showShiftTimes = false">close</span>
</div> </div>
<p class="text-xs py-1.5 text-gray-300 max-w-md">Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.</p> <p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
</div> </div>
<div class="w-40"></div> <div class="w-40"></div>
</div> </div>
@@ -49,8 +49,8 @@
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2"> <div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="w-12"></div> <div class="w-12"></div>
<div class="w-32 px-2">Start</div> <div class="w-32 px-2">{{ $strings.LabelStart }}</div>
<div class="flex-grow px-2">Title</div> <div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
<div class="w-40"></div> <div class="w-40"></div>
</div> </div>
<template v-for="chapter in newChapters"> <template v-for="chapter in newChapters">
@@ -69,7 +69,7 @@
<span class="material-icons-outlined text-base">remove</span> <span class="material-icons-outlined text-base">remove</span>
</button> </button>
<ui-tooltip text="Insert chapter below" direction="bottom"> <ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)"> <button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
<span class="material-icons text-lg">add</span> <span class="material-icons text-lg">add</span>
</button> </button>
@@ -93,11 +93,11 @@
</div> </div>
<div class="w-full max-w-xl py-4"> <div class="w-full max-w-xl py-4">
<p class="text-lg mb-4 font-semibold py-1">Audio Tracks</p> <p class="text-lg mb-4 font-semibold py-1">{{ $strings.HeaderAudioTracks }}</p>
<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">Filename</div> <div class="flex-grow">{{ $strings.LabelFilename }}</div>
<div class="w-20">Duration</div> <div class="w-20">{{ $strings.LabelDuration }}</div>
<div class="w-20 text-center">Chapters</div> <div class="w-20 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' : ''">
@@ -122,34 +122,34 @@
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters"> <modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none"> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">Find Chapters</p> <p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
</div> </div>
</template> </template>
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative"> <div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
<div v-if="!chapterData" class="flex p-20"> <div v-if="!chapterData" class="flex p-20">
<ui-text-input-with-label v-model="asinInput" label="ASIN" /> <ui-text-input-with-label v-model="asinInput" label="ASIN" />
<ui-dropdown v-model="regionInput" label="Region" small :items="audibleRegions" class="w-32 mx-1" /> <ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
<ui-btn small color="primary" class="mt-5" @click="findChapters">Find</ui-btn> <ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
</div> </div>
<div v-else class="w-full p-4"> <div v-else class="w-full p-4">
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
<p> <p>
Duration found: <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span {{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span
><br /> ><br />
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> chapters found <span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> {{ $strings.LabelChaptersFound }}
</p> </p>
<p> <p>
Your audiobook duration: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span {{ $strings.LabelYourAudiobookDuration }}: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
><br /> ><br />
Your audiobook has <span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapters.length }}</span> chapters Your audiobook has <span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapters.length }}</span> chapters
</p> </p>
</div> </div>
<widgets-alert v-if="chapterData.runtimeLengthSec > mediaDurationRounded" type="warning" class="mb-2"> Your audiobook duration is shorter than duration found </widgets-alert> <widgets-alert v-if="chapterData.runtimeLengthSec > mediaDurationRounded" type="warning" class="mb-2"> {{ $strings.MessageYourAudiobookDurationIsShorter }} </widgets-alert>
<widgets-alert v-else-if="chapterData.runtimeLengthSec < mediaDurationRounded" type="warning" class="mb-2"> Your audiobook duration is longer than the duration found </widgets-alert> <widgets-alert v-else-if="chapterData.runtimeLengthSec < mediaDurationRounded" type="warning" class="mb-2"> {{ $strings.MessageYourAudiobookDurationIsLonger }} </widgets-alert>
<div class="flex py-0.5 text-xs font-semibold uppercase text-gray-300 mb-1"> <div class="flex py-0.5 text-xs font-semibold uppercase text-gray-300 mb-1">
<div class="w-24 px-2">Start</div> <div class="w-24 px-2">{{ $strings.LabelStart }}</div>
<div class="flex-grow px-2">Title</div> <div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
</div> </div>
<div class="w-full max-h-80 overflow-y-auto my-2"> <div class="w-full max-h-80 overflow-y-auto my-2">
<div v-for="(chapter, index) in chapterData.chapters" :key="index" class="flex py-0.5 text-xs" :class="chapter.startOffsetSec > mediaDuration ? 'bg-error bg-opacity-20' : chapter.startOffsetSec + chapter.lengthMs / 1000 > mediaDuration ? 'bg-warning bg-opacity-20' : index % 2 === 0 ? 'bg-primary bg-opacity-30' : ''"> <div v-for="(chapter, index) in chapterData.chapters" :key="index" class="flex py-0.5 text-xs" :class="chapter.startOffsetSec > mediaDuration ? 'bg-error bg-opacity-20' : chapter.startOffsetSec + chapter.lengthMs / 1000 > mediaDuration ? 'bg-warning bg-opacity-20' : index % 2 === 0 ? 'bg-primary bg-opacity-30' : ''">
@@ -164,20 +164,20 @@
<div v-if="chapterData.runtimeLengthSec > mediaDurationRounded" class="w-full pt-2"> <div v-if="chapterData.runtimeLengthSec > mediaDurationRounded" class="w-full pt-2">
<div class="flex items-center"> <div class="flex items-center">
<div class="w-2 h-2 bg-warning bg-opacity-50" /> <div class="w-2 h-2 bg-warning bg-opacity-50" />
<p class="pl-2">Chapter end is after the end of your audiobook</p> <p class="pl-2">{{ $strings.MessageChapterEndIsAfter }}</p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-2 h-2 bg-error bg-opacity-50" /> <div class="w-2 h-2 bg-error bg-opacity-50" />
<p class="pl-2">Chapter start is after the end of your audiobook</p> <p class="pl-2">{{ $strings.MessageChapterStartIsAfter }}</p>
</div> </div>
</div> </div>
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">Map Chapter Titles</ui-btn> <ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip text="Map chapter titles to your existing audiobook chapters without adjusting timestamps" direction="top"> <ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top">
<span class="material-icons-outlined">info</span> <span class="material-icons-outlined">info</span>
</ui-tooltip> </ui-tooltip>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small color="success" @click="applyChapterData">Apply Chapters</ui-btn> <ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -442,7 +442,7 @@ export default {
this.$router.push(`/item/${this.libraryItem.id}`) this.$router.push(`/item/${this.libraryItem.id}`)
} }
} else { } else {
this.$toast.info('No changes needed updating') this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
+27 -39
View File
@@ -5,73 +5,68 @@
</div> </div>
<div class="w-full h-full overflow-y-auto p-8"> <div class="w-full h-full overflow-y-auto p-8">
<div class="w-full flex justify-between items-center pb-6 pt-2"> <div class="w-full flex justify-between items-center pb-6 pt-2">
<p class="text-lg">Drag files into correct track order</p> <p class="text-lg">{{ $strings.MessageDragFilesIntoTrackOrder }}</p>
<ui-btn color="success" @click="saveTracklist">Save Tracklist</ui-btn> <ui-btn color="success" @click="saveTracklist">{{ $strings.ButtonSaveTracklist }}</ui-btn>
</div> </div>
<div class="w-full flex items-center text-sm py-4 bg-primary border-l border-r border-t border-gray-600"> <div class="w-full flex items-center text-sm py-4 bg-primary border-l border-r border-t border-gray-600">
<div class="font-book text-center px-4 w-12">New</div> <div class="text-center px-4 w-12">{{ $strings.LabelNew }}</div>
<div class="font-book text-center px-4 w-24 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByCurrent" @mousedown.prevent> <div class="text-center px-4 w-24 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByCurrent" @mousedown.prevent>
<span class="text-white">Current</span> <span class="text-white">{{ $strings.LabelCurrent }}</span>
<span class="material-icons ml-1" :class="currentSort === 'current' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }}</span> <span class="material-icons ml-1" :class="currentSort === 'current' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }}</span>
</div> </div>
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilenameTrack" @mousedown.prevent> <div class="text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilenameTrack" @mousedown.prevent>
<span class="text-white">Track From Filename</span> <span class="text-white">{{ $strings.LabelTrackFromFilename }}</span>
<span class="material-icons ml-1" :class="currentSort === 'track-filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}</span> <span class="material-icons ml-1" :class="currentSort === 'track-filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}</span>
</div> </div>
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByMetadataTrack" @mousedown.prevent> <div class="text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByMetadataTrack" @mousedown.prevent>
<span class="text-white">Track From Metadata</span> <span class="text-white">{{ $strings.LabelTrackFromMetadata }}</span>
<span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span> <span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span>
</div> </div>
<div class="font-mono w-20 text-center">Disc From Filename</div> <div class="w-20 text-center">{{ $strings.LabelDiscFromFilename }}</div>
<div class="font-mono w-20 text-center">Disc From Metadata</div> <div class="w-20 text-center">{{ $strings.LabelDiscFromMetadata }}</div>
<div class="font-book text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent> <div class="text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent>
<span class="text-white">Filename</span> <span class="text-white">{{ $strings.LabelFilename }}</span>
<span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span> <span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span>
</div> </div>
<!-- <div class="font-book truncate px-4 flex-grow">Filename</div> -->
<div class="font-mono w-20 text-center">Size</div> <div class="w-20 text-center">{{ $strings.LabelSize }}</div>
<div class="font-mono w-20 text-center">Duration</div> <div class="w-20 text-center">{{ $strings.LabelDuration }}</div>
<div class="font-mono text-center w-20">Status</div> <div class="w-56">{{ $strings.LabelNotes }}</div>
<div class="font-mono w-56">Notes</div> <div class="w-40">{{ $strings.LabelIncludeInTracklist }}</div>
<div class="font-book w-40">Include in Tracklist</div>
</div> </div>
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate"> <draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'flip-list' : null"> <transition-group type="transition" :name="!drag ? 'flip-list' : null">
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative"> <li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
<div class="font-book text-center px-4 py-1 w-12"> <div class="font-book text-center px-4 py-1 w-12 min-w-12">
{{ audio.include ? index - numExcluded + 1 : -1 }} {{ audio.include ? index - numExcluded + 1 : -1 }}
</div> </div>
<div class="font-book text-center px-4 w-24">{{ audio.index }}</div> <div class="font-book text-center px-4 w-24 min-w-24">{{ audio.index }}</div>
<div class="font-book text-center px-2 w-32"> <div class="font-book text-center px-2 w-32 min-w-32">
{{ audio.trackNumFromFilename }} {{ audio.trackNumFromFilename }}
</div> </div>
<div class="font-book text-center w-32"> <div class="font-book text-center w-32 min-w-32">
{{ audio.trackNumFromMeta }} {{ audio.trackNumFromMeta }}
</div> </div>
<div class="font-book truncate px-4 w-20"> <div class="font-book truncate px-4 w-20 min-w-20">
{{ audio.discNumFromFilename }} {{ audio.discNumFromFilename }}
</div> </div>
<div class="font-book truncate px-4 w-20"> <div class="font-book truncate px-4 w-20 min-w-20">
{{ audio.discNumFromMeta }} {{ audio.discNumFromMeta }}
</div> </div>
<div class="font-book truncate px-4 flex-grow"> <div class="font-book truncate px-4 flex-grow">
{{ audio.metadata.filename }} {{ audio.metadata.filename }}
</div> </div>
<div class="font-mono w-20 text-center"> <div class="font-mono w-20 min-w-20 text-center text-xs">
{{ $bytesPretty(audio.metadata.size) }} {{ $bytesPretty(audio.metadata.size) }}
</div> </div>
<div class="font-mono w-20"> <div class="font-mono w-20 min-w-20 text-center text-xs">
{{ $secondsToTimestamp(audio.duration) }} {{ $secondsToTimestamp(audio.duration) }}
</div> </div>
<div class="font-mono text-center w-20"> <div class="font-sans text-xs font-normal w-56 min-w-[224px]">
<span class="material-icons text-sm" :class="audio.invalid ? 'text-error' : 'text-success'">{{ getStatusIcon(audio) }}</span>
</div>
<div class="font-sans text-xs font-normal w-56">
{{ audio.error }} {{ audio.error }}
</div> </div>
<div class="font-sans text-xs font-normal w-40 flex items-center justify-center"> <div class="font-sans text-xs font-normal w-40 min-w-[160px] flex items-center justify-center">
<ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" /> <ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" />
</div> </div>
</li> </li>
@@ -229,13 +224,6 @@ export default {
console.error('Failed', error) console.error('Failed', error)
this.saving = false this.saving = false
}) })
},
getStatusIcon(audio) {
if (audio.invalid) {
return 'error_outline'
} else {
return 'check_circle'
}
} }
} }
} }
+18 -18
View File
@@ -2,7 +2,7 @@
<div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''"> <div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
<div class="flex items-center justify-center mb-6"> <div class="flex items-center justify-center mb-6">
<div class="w-full max-w-2xl"> <div class="w-full max-w-2xl">
<p class="text-2xl mb-2">Audiobook File Management Tools</p> <p class="text-2xl mb-2">{{ $strings.HeaderAudiobookTools }}</p>
</div> </div>
<div class="w-full max-w-2xl"> <div class="w-full max-w-2xl">
<div class="flex justify-end"> <div class="flex justify-end">
@@ -13,7 +13,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
<div class="w-full max-w-2xl"> <div class="w-full max-w-2xl">
<p class="text-xl mb-1">Metadata to embed</p> <p class="text-xl mb-1">{{ $strings.HeaderMetadataToEmbed }}</p>
<p class="mb-2 text-base text-gray-300">audiobookshelf uses <a href="https://github.com/sandreas/tone" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">tone</a> to write metadata.</p> <p class="mb-2 text-base text-gray-300">audiobookshelf uses <a href="https://github.com/sandreas/tone" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">tone</a> to write metadata.</p>
</div> </div>
<div class="w-full max-w-2xl"></div> <div class="w-full max-w-2xl"></div>
@@ -22,8 +22,8 @@
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap">
<div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2"> <div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2">
<div class="flex py-2 px-4"> <div class="flex py-2 px-4">
<div class="w-1/3 text-xs font-semibold uppercase text-gray-200">Meta Tag</div> <div class="w-1/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelMetaTag }}</div>
<div class="w-2/3 text-xs font-semibold uppercase text-gray-200">Value</div> <div class="w-2/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
</div> </div>
<div class="w-full max-h-72 overflow-auto"> <div class="w-full max-h-72 overflow-auto">
<template v-for="(value, key, index) in toneObject"> <template v-for="(value, key, index) in toneObject">
@@ -38,12 +38,12 @@
</div> </div>
<div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2"> <div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2">
<div class="flex py-2 px-4 bg-primary bg-opacity-25"> <div class="flex py-2 px-4 bg-primary bg-opacity-25">
<div class="flex-grow text-xs font-semibold uppercase text-gray-200">Chapter Title</div> <div class="flex-grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelChapterTitle }}</div>
<div class="w-24 text-xs font-semibold uppercase text-gray-200">Start</div> <div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelStart }}</div>
<div class="w-24 text-xs font-semibold uppercase text-gray-200">End</div> <div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelEnd }}</div>
</div> </div>
<div class="w-full max-h-72 overflow-auto"> <div class="w-full max-h-72 overflow-auto">
<p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">No chapters</p> <p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">{{ $strings.MessageNoChapters }}</p>
<template v-for="(chapter, index) in metadataChapters"> <template v-for="(chapter, index) in metadataChapters">
<div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary bg-opacity-25' : ''"> <div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary bg-opacity-25' : ''">
<div class="flex-grow font-semibold">{{ chapter.title }}</div> <div class="flex-grow font-semibold">{{ chapter.title }}</div>
@@ -63,20 +63,20 @@
<div class="w-full max-w-4xl mx-auto"> <div class="w-full max-w-4xl mx-auto">
<div v-if="selectedTool === 'embed'" class="w-full flex justify-end items-center mb-4"> <div v-if="selectedTool === 'embed'" class="w-full flex justify-end items-center mb-4">
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">Start Metadata Embed</ui-btn> <ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
<p v-else class="text-success text-lg font-semibold">Embed Finished!</p> <p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
</div> </div>
<div v-else class="w-full flex justify-end items-center mb-4"> <div v-else class="w-full flex justify-end items-center mb-4">
<ui-btn v-if="!isTaskFinished && processing" color="error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">Cancel Encode</ui-btn> <ui-btn v-if="!isTaskFinished && processing" color="error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">{{ $strings.ButtonCancelEncode }}</ui-btn>
<ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">Start M4B Encode</ui-btn> <ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">{{ $strings.ButtonStartM4BEncode }}</ui-btn>
<p v-else-if="taskFailed" class="text-error text-lg font-semibold">M4B Failed! {{ taskError }}</p> <p v-else-if="taskFailed" class="text-error text-lg font-semibold">{{ $strings.MessageM4BFailed }} {{ taskError }}</p>
<p v-else class="text-success text-lg font-semibold">M4B Finished!</p> <p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<div v-if="selectedTool === 'embed'" class="flex items-start mb-2"> <div v-if="selectedTool === 'embed'" class="flex items-start mb-2">
<span class="material-icons text-base text-warning pt-1">star</span> <span class="material-icons text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">Metadata will be embedded on the audio tracks inside your audiobook folder.</p> <p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p>
</div> </div>
<div v-else class="flex items-start mb-2"> <div v-else class="flex items-start mb-2">
<span class="material-icons text-base text-warning pt-1">star</span> <span class="material-icons text-base text-warning pt-1">star</span>
@@ -111,12 +111,12 @@
</div> </div>
<div class="w-full max-w-4xl mx-auto"> <div class="w-full max-w-4xl mx-auto">
<p class="mb-2 font-semibold">Audio Tracks</p> <p class="mb-2 font-semibold">{{ $strings.HeaderAudioTracks }}</p>
<div class="w-full mx-auto border border-white border-opacity-10 bg-bg"> <div class="w-full mx-auto border border-white border-opacity-10 bg-bg">
<div class="flex py-2 px-4 bg-primary bg-opacity-25"> <div class="flex py-2 px-4 bg-primary bg-opacity-25">
<div class="w-10 text-xs font-semibold text-gray-200">#</div> <div class="w-10 text-xs font-semibold text-gray-200">#</div>
<div class="flex-grow text-xs font-semibold uppercase text-gray-200">Filename</div> <div class="flex-grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelFilename }}</div>
<div class="w-16 text-xs font-semibold uppercase text-gray-200">Size</div> <div class="w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelSize }}</div>
<div class="w-24"></div> <div class="w-24"></div>
</div> </div>
<template v-for="file in audioFiles"> <template v-for="file in audioFiles">
+5 -5
View File
@@ -16,25 +16,25 @@
</button> </button>
</div> </div>
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">Description</p> <p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
<p class="text-white max-w-3xl text-sm leading-5">{{ author.description }}</p> <p class="text-white max-w-3xl text-sm leading-5">{{ author.description }}</p>
</div> </div>
</div> </div>
<div class="py-4"> <div class="py-4">
<widgets-item-slider :items="libraryItems" :bookshelf-view="$constants.BookshelfView.AUTHOR"> <widgets-item-slider :items="libraryItems" shelf-id="author-books" :bookshelf-view="$constants.BookshelfView.AUTHOR">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">
<h2 class="text-lg">{{ libraryItems.length }} Books</h2> <h2 class="text-lg">{{ libraryItems.length }} {{ $strings.LabelBooks }}</h2>
</nuxt-link> </nuxt-link>
</widgets-item-slider> </widgets-item-slider>
</div> </div>
<div v-for="series in authorSeries" :key="series.id" class="py-4"> <div v-for="series in authorSeries" :key="series.id" class="py-4">
<widgets-item-slider :items="series.items" :bookshelf-view="$constants.BookshelfView.AUTHOR"> <widgets-item-slider :items="series.items" :shelf-id="series.id" :bookshelf-view="$constants.BookshelfView.AUTHOR">
<nuxt-link :to="`/library/${currentLibraryId}/series/${series.id}`" class="hover:underline"> <nuxt-link :to="`/library/${currentLibraryId}/series/${series.id}`" class="hover:underline">
<h2 class="text-lg">{{ series.name }}</h2> <h2 class="text-lg">{{ series.name }}</h2>
</nuxt-link> </nuxt-link>
<p class="text-white text-opacity-40 text-base px-2">Series</p> <p class="text-white text-opacity-40 text-base px-2">{{ $strings.LabelSeries }}</p>
</widgets-item-slider> </widgets-item-slider>
</div> </div>
</div> </div>
+13 -13
View File
@@ -11,47 +11,47 @@
<div v-if="openMapOptions" class="flex flex-wrap"> <div v-if="openMapOptions" class="flex flex-wrap">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.subtitle" /> <ui-checkbox v-model="selectedBatchUsage.subtitle" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" label="Subtitle" class="mb-4 ml-4" /> <ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
</div> </div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.authors" /> <ui-checkbox v-model="selectedBatchUsage.authors" />
<!-- Authors filter only contains authors in this library, use query input to query all authors --> <!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" /> <ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
</div> </div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publishedYear" /> <ui-checkbox v-model="selectedBatchUsage.publishedYear" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" label="Publish Year" class="mb-4 ml-4" /> <ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
</div> </div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.series" /> <ui-checkbox v-model="selectedBatchUsage.series" />
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" label="Series" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" /> <ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
</div> </div>
<div class="flex items-center px-4 w-1/2"> <div class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.genres" /> <ui-checkbox v-model="selectedBatchUsage.genres" />
<ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" label="Genres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-4 ml-4" /> <ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-4 ml-4" />
</div> </div>
<div class="flex items-center px-4 w-1/2"> <div class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.tags" /> <ui-checkbox v-model="selectedBatchUsage.tags" />
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" label="Tags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" /> <ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
</div> </div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.narrators" /> <ui-checkbox v-model="selectedBatchUsage.narrators" />
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" label="Narrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" /> <ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
</div> </div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publisher" /> <ui-checkbox v-model="selectedBatchUsage.publisher" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" label="Publisher" class="mb-4 ml-4" /> <ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
</div> </div>
<div class="flex items-center px-4 w-1/2"> <div class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.language" /> <ui-checkbox v-model="selectedBatchUsage.language" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" label="Language" class="mb-4 ml-4" /> <ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
</div> </div>
<div class="flex items-center px-4 w-1/2"> <div class="flex items-center px-4 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.explicit" /> <ui-checkbox v-model="selectedBatchUsage.explicit" />
<div class="ml-4"> <div class="ml-4">
<ui-checkbox <ui-checkbox
v-model="batchDetails.explicit" v-model="batchDetails.explicit"
label="Explicit" :label="$strings.LabelExplicit"
:disabled="!selectedBatchUsage.explicit" :disabled="!selectedBatchUsage.explicit"
:checkbox-bg="!selectedBatchUsage.explicit ? 'bg' : 'primary'" :checkbox-bg="!selectedBatchUsage.explicit ? 'bg' : 'primary'"
:check-color="!selectedBatchUsage.explicit ? 'gray-600' : 'green-500'" :check-color="!selectedBatchUsage.explicit ? 'gray-600' : 'green-500'"
@@ -62,7 +62,7 @@
</div> </div>
<div class="w-full flex items-center justify-end p-4"> <div class="w-full flex items-center justify-end p-4">
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">Apply</ui-btn> <ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
</div> </div>
</div> </div>
</transition> </transition>
@@ -83,7 +83,7 @@
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }"> <div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">Save</ui-btn> <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -342,7 +342,7 @@ export default {
this.$toast.success(`Successfully updated ${data.updates} items`) this.$toast.success(`Successfully updated ${data.updates} items`)
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
} else { } else {
this.$toast.warning('No updates were necessary') this.$toast.warning(this.$strings.MessageNoUpdatesWereNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
+37 -8
View File
@@ -16,7 +16,7 @@
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay"> <ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }} {{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
</ui-btn> </ui-btn>
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" /> <ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
@@ -52,7 +52,7 @@ export default {
return redirect('/') return redirect('/')
} }
store.commit('user/addUpdateCollection', collection) store.commit('libraries/addUpdateCollection', collection)
return { return {
collectionId: collection.id collectionId: collection.id
} }
@@ -80,7 +80,7 @@ export default {
return this.collection.description || '' return this.collection.description || ''
}, },
collection() { collection() {
return this.$store.getters['user/getCollection'](this.collectionId) return this.$store.getters['libraries/getCollection'](this.collectionId) || {}
}, },
playableBooks() { playableBooks() {
return this.bookItems.filter((book) => { return this.bookItems.filter((book) => {
@@ -122,13 +122,42 @@ export default {
} }
}, },
clickPlay() { clickPlay() {
var nextBookNotRead = this.playableBooks.find((pb) => { const queueItems = []
var prog = this.$store.getters['user/getUserMediaProgress'](pb.id)
return !prog || !prog.isFinished // Collection queue will start at the first unfinished book
// if all books are finished then entire collection is queued
const itemsWithProgress = this.playableBooks.map((item) => {
return {
...item,
progress: this.$store.getters['user/getUserMediaProgress'](item.id)
}
}) })
if (nextBookNotRead) {
const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished)
if (!hasUnfinishedItems) {
console.warn('All items in collection are finished - starting at first item')
}
for (let i = 0; i < itemsWithProgress.length; i++) {
const libraryItem = itemsWithProgress[i]
if (!hasUnfinishedItems || !libraryItem.progress || !libraryItem.progress.isFinished) {
queueItems.push({
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
episodeId: null,
title: libraryItem.media.metadata.title,
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
caption: '',
duration: libraryItem.media.duration || null,
coverPath: libraryItem.media.coverPath || null
})
}
}
if (queueItems.length >= 0) {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: nextBookNotRead.id libraryItemId: queueItems[0].libraryItemId,
queueItems
}) })
} }
} }
+10 -12
View File
@@ -2,15 +2,16 @@
<div class="w-full h-full"> <div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">Backups</h1> <h1 class="text-xl">{{ $strings.HeaderBackups }}</h1>
</div> </div>
<p class="text-base mb-4 text-gray-300">Backups include users, user progress, library item details, server settings, and images stored in <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>. <br />Backups <strong>do not</strong> include any files stored in your library folders.</p> <p class="text-base mb-2 text-gray-300">{{ $strings.MessageBackupsDescription }} <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>.</p>
<p class="text-base mb-4 text-gray-300">{{ $strings.MessageBackupsNote }}</p>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" /> <ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
<ui-tooltip :text="backupsTooltip"> <ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
<p class="pl-4 text-lg">Enable automatic backups <span class="material-icons icon-text">info_outlined</span></p> <p class="pl-4 text-lg">{{ $strings.LabelBackupsEnableAutomaticBackups }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -25,16 +26,16 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" /> <ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
<ui-tooltip :text="numBackupsToKeepTooltip"> <ui-tooltip :text="$strings.LabelBackupsNumberToKeepHelp">
<p class="pl-4 text-lg">Number of backups to keep <span class="material-icons icon-text">info_outlined</span></p> <p class="pl-4 text-lg">{{ $strings.LabelBackupsNumberToKeep }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-text-input type="number" v-model="maxBackupSize" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" /> <ui-text-input type="number" v-model="maxBackupSize" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
<ui-tooltip :text="maxBackupSizeTooltip"> <ui-tooltip :text="$strings.LabelBackupsMaxBackupSizeHelp">
<p class="pl-4 text-lg">Maximum backup size (in GB) <span class="material-icons icon-text">info_outlined</span></p> <p class="pl-4 text-lg">{{ $strings.LabelBackupsMaxBackupSize }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -55,10 +56,7 @@ export default {
maxBackupSize: 1, maxBackupSize: 1,
cronExpression: '', cronExpression: '',
newServerSettings: {}, newServerSettings: {},
showCronBuilder: false, showCronBuilder: false
backupsTooltip: 'Backups saved in /metadata/backups',
numBackupsToKeepTooltip: 'Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.',
maxBackupSizeTooltip: 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
} }
}, },
watch: { watch: {
+57 -91
View File
@@ -2,19 +2,19 @@
<div> <div>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
<div class="mb-2"> <div class="mb-2">
<h1 class="text-xl">Settings</h1> <h1 class="text-xl">{{ $strings.HeaderSettings }}</h1>
</div> </div>
<div class="lg:flex"> <div class="lg:flex">
<div class="flex-1"> <div class="flex-1">
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">General</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
</div> </div>
<div class="flex items-end py-2"> <div class="flex items-end py-2">
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" /> <ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
<ui-tooltip :text="tooltips.storeCoverWithItem"> <ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
<p class="pl-4"> <p class="pl-4">
Store covers with item {{ $strings.LabelSettingsStoreCoversWithItem }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -22,9 +22,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" /> <ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
<ui-tooltip :text="tooltips.storeMetadataWithItem"> <ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
<p class="pl-4"> <p class="pl-4">
Store metadata with item {{ $strings.LabelSettingsStoreMetadataWithItem }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -32,31 +32,31 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" /> <ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
<ui-tooltip :text="tooltips.sortingIgnorePrefix"> <ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
<p class="pl-4"> <p class="pl-4">
Ignore prefixes when sorting {{ $strings.LabelSettingsSortingIgnorePrefixes }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2"> <div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" /> <ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" /> <ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
<p class="pl-4">Chromecast support</p> <p class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
</div> </div>
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">Display</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" /> <ui-toggle-switch v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
<ui-tooltip :text="tooltips.bookshelfView"> <ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
<p class="pl-4"> <p class="pl-4">
Home page use bookshelf view {{ $strings.LabelSettingsHomePageBookshelfView }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -64,30 +64,35 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" /> <ui-toggle-switch v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
<ui-tooltip :text="tooltips.bookshelfView"> <ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
<p class="pl-4"> <p class="pl-4">
Library use bookshelf view {{ $strings.LabelSettingsLibraryBookshelfView }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="py-2">
<p class="pr-4">Date Format</p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelSettingsDateFormat }}</p>
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-40" @input="(val) => updateSettingsKey('dateFormat', val)" /> <ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
</div>
<div class="py-2">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguageDefaultServer }}</p>
<ui-dropdown ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
</div> </div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">Scanner</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip :text="tooltips.scannerParseSubtitle"> <ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
<p class="pl-4"> <p class="pl-4">
Parse subtitles {{ $strings.LabelSettingsParseSubtitles }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -95,9 +100,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip :text="tooltips.scannerFindCovers"> <ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
<p class="pl-4"> <p class="pl-4">
Find covers {{ $strings.LabelSettingsFindCovers }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -109,9 +114,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker"> <ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
<p class="pl-4"> <p class="pl-4">
Use Overdrive Media Markers for chapters {{ $strings.LabelSettingsOverdriveMediaMarkers }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -119,9 +124,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferAudioMetadata"> <ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
<p class="pl-4"> <p class="pl-4">
Prefer audio metadata {{ $strings.LabelSettingsPreferAudioMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -129,9 +134,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata"> <ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
<p class="pl-4"> <p class="pl-4">
Prefer OPF metadata {{ $strings.LabelSettingsPreferOPFMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -139,9 +144,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata"> <ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4"> <p class="pl-4">
Prefer matched metadata {{ $strings.LabelSettingsPreferMatchedMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -149,33 +154,23 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
<ui-tooltip :text="tooltips.scannerDisableWatcher"> <ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
<p class="pl-4"> <p class="pl-4">
Disable Watcher {{ $strings.LabelSettingsDisableWatcher }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<!-- <div class="flex items-center py-2">
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
<ui-tooltip :text="tooltips.scannerMaxThreads">
<p class="pl-4">
Max # of threads to use
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">Experimental Features</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="showExperimentalFeatures" /> <ui-toggle-switch v-model="showExperimentalFeatures" />
<ui-tooltip :text="tooltips.experimentalFeatures"> <ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
<p class="pl-4"> <p class="pl-4">
Experimental Features {{ $strings.LabelSettingsExperimentalFeatures }}
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank"> <a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</a> </a>
@@ -185,9 +180,9 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" /> <ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
<ui-tooltip :text="tooltips.enableEReader"> <ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
<p class="pl-4"> <p class="pl-4">
Enable e-reader for all users {{ $strings.LabelSettingsEnableEReader }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -210,14 +205,14 @@
<div class="flex items-center py-4"> <div class="flex items-center py-4">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeCache">Purge All Cache</ui-btn> <ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeCache">{{ $strings.ButtonPurgeAllCache }}</ui-btn>
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeItemsCache">Purge Items Cache</ui-btn> <ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</ui-btn>
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn> <ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">{{ $strings.ButtonRemoveAllLibraryItems }}</ui-btn>
</div> </div>
<div class="flex items-center py-4"> <div class="flex items-center py-4">
<div class="flex-grow" /> <div class="flex-grow" />
<p class="pr-2 text-sm font-book text-yellow-400"> <p class="pr-2 text-sm font-book text-yellow-400">
Report bugs, request features, and contribute on {{ $strings.MessageReportBugsAndContribute }}
<a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a> <a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>
</p> </p>
<a href="https://github.com/advplyr/audiobookshelf" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> <a href="https://github.com/advplyr/audiobookshelf" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
@@ -228,7 +223,7 @@
</svg> </svg>
</a> </a>
<p class="pl-4 pr-2 text-sm font-book text-yellow-400"> <p class="pl-4 pr-2 text-sm font-book text-yellow-400">
Join us on {{ $strings.MessageJoinUsOn }}
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a> <a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
</p> </p>
<a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> <a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
@@ -276,23 +271,6 @@ export default {
useBookshelfView: false, useBookshelfView: false,
isPurgingCache: false, isPurgingCache: false,
newServerSettings: {}, newServerSettings: {},
tooltips: {
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
scannerPreferMatchedMetadata: 'Matched data will overide book details when using Quick Match',
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
scannerFindCovers: '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',
bookshelfView: 'Skeumorphic design with wooden shelves',
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
enableEReader: '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)',
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
},
showConfirmPurgeCache: false showConfirmPurgeCache: false
} }
}, },
@@ -323,26 +301,6 @@ export default {
} }
}, },
methods: { methods: {
updateScannerMaxThreads(val) {
if (!val || isNaN(val)) {
this.$toast.error('Invalid max threads must be a number')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Number(val) < 0) {
this.$toast.error('Max threads must be >= 0')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Math.round(Number(val)) !== Number(val)) {
this.$toast.error('Max threads must be an integer')
this.newServerSettings.scannerMaxThreads = 0
return
}
this.updateServerSettings({
scannerMaxThreads: Number(val)
})
},
updateSortingPrefixes(val) { updateSortingPrefixes(val) {
if (!val || !val.length) { if (!val || !val.length) {
this.$toast.error('Must have at least 1 prefix') this.$toast.error('Must have at least 1 prefix')
@@ -368,6 +326,9 @@ export default {
bookshelfView: !val ? this.$constants.BookshelfView.DETAIL : this.$constants.BookshelfView.STANDARD bookshelfView: !val ? this.$constants.BookshelfView.DETAIL : this.$constants.BookshelfView.STANDARD
}) })
}, },
updateServerLanguage(val) {
this.updateSettingsKey('language', val)
},
updateSettingsKey(key, val) { updateSettingsKey(key, val) {
this.updateServerSettings({ this.updateServerSettings({
[key]: val [key]: val
@@ -381,6 +342,11 @@ export default {
console.log('Updated Server Settings', success) console.log('Updated Server Settings', success)
this.updatingServerSettings = false this.updatingServerSettings = false
this.$toast.success('Server settings updated') this.$toast.success('Server settings updated')
if (payload.language) {
// Updating language after save allows for re-rendering
this.$setLanguageCode(payload.language)
}
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
@@ -396,7 +362,7 @@ export default {
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
}, },
resetLibraryItems() { resetLibraryItems() {
if (confirm('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. Shall we continue?')) { if (confirm(this.$strings.MessageRemoveAllItemsWarning)) {
this.isResettingLibraryItems = true this.isResettingLibraryItems = true
this.$axios this.$axios
.$delete('/api/items/all') .$delete('/api/items/all')
+7 -7
View File
@@ -1,12 +1,12 @@
<template> <template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1> <h1 class="text-xl">{{ $strings.HeaderLibraryStats }}: {{ currentLibraryName }}</h1>
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" /> <stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8"> <div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1> <h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
<p v-if="!top5Genres.length">No Genres</p> <p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
<template v-for="genre in top5Genres"> <template v-for="genre in top5Genres">
<div :key="genre.genre" class="w-full py-2"> <div :key="genre.genre" class="w-full py-2">
<div class="flex items-end mb-1"> <div class="flex items-end mb-1">
@@ -23,8 +23,8 @@
</template> </template>
</div> </div>
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 10 Authors</h1> <h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
<p v-if="!top10Authors.length">No Authors</p> <p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
<template v-for="(author, index) in top10Authors"> <template v-for="(author, index) in top10Authors">
<div :key="author.id" class="w-full py-2"> <div :key="author.id" class="w-full py-2">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
@@ -42,8 +42,8 @@
</template> </template>
</div> </div>
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Longest Items (hrs)</h1> <h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
<p v-if="!top10LongestItems.length">No Items</p> <p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
<template v-for="(ab, index) in top10LongestItems"> <template v-for="(ab, index) in top10LongestItems">
<div :key="index" class="w-full py-2"> <div :key="index" class="w-full py-2">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
+2 -2
View File
@@ -2,7 +2,7 @@
<div class="w-full h-full"> <div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">Logs</h1> <h1 class="text-xl">{{ $strings.HeaderLogs }}</h1>
</div> </div>
<div class="flex justify-between mb-2 place-items-end"> <div class="flex justify-between mb-2 place-items-end">
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" /> <ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
@@ -22,7 +22,7 @@
</div> </div>
<div v-if="!logs.length" class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-center"> <div v-if="!logs.length" class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-center">
<p class="text-xl text-gray-200 mb-2">No Logs</p> <p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
</div> </div>
</div> </div>
</div> </div>
+9 -9
View File
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto">
<h2 class="text-xl font-semibold mb-4">Apprise Notification Settings</h2> <h2 class="text-xl font-semibold mb-4">{{ $strings.HeaderAppriseNotificationSettings }}</h2>
<p class="mb-6 text-gray-200"> <p class="mb-6 text-gray-200">
In order to use this feature you will need to have an instance of <a href="https://github.com/caronc/apprise-api" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at In order to use this feature you will need to have an instance of <a href="https://github.com/caronc/apprise-api" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
<span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337</span> then you would put <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337/notify</span>. <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337</span> then you would put <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337/notify</span>.
@@ -13,33 +13,33 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-text-input ref="maxNotificationQueueInput" type="number" v-model="maxNotificationQueue" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" /> <ui-text-input ref="maxNotificationQueueInput" type="number" v-model="maxNotificationQueue" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" />
<ui-tooltip text="Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming." direction="right"> <ui-tooltip :text="$strings.LabelNotificationsMaxQueueSizeHelp" direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">Max queue size for notification events<span class="material-icons icon-text ml-1">info_outlined</span></p> <p class="pl-2 md:pl-4 text-base md:text-lg">{{ $strings.LabelNotificationsMaxQueueSize }}<span class="material-icons icon-text ml-1">info_outlined</span></p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-text-input ref="maxFailedAttemptsInput" type="number" v-model="maxFailedAttempts" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" /> <ui-text-input ref="maxFailedAttemptsInput" type="number" v-model="maxFailedAttempts" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" />
<ui-tooltip text="Notifications are disabled once they fail to send this many times." direction="right"> <ui-tooltip :text="$strings.LabelNotificationsMaxFailedAttemptsHelp" direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">Max failed attempts<span class="material-icons icon-text ml-1">info_outlined</span></p> <p class="pl-2 md:pl-4 text-base md:text-lg">{{ $strings.LabelNotificationsMaxFailedAttempts }}<span class="material-icons icon-text ml-1">info_outlined</span></p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center justify-end pt-4"> <div class="flex items-center justify-end pt-4">
<ui-btn :loading="savingSettings" type="submit">Save</ui-btn> <ui-btn :loading="savingSettings" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</form> </form>
<div class="w-full h-px bg-white bg-opacity-10 my-6" /> <div class="w-full h-px bg-white bg-opacity-10 my-6" />
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold">Notifications</h2> <h2 class="text-xl font-semibold">{{ $strings.HeaderNotifications }}</h2>
<ui-btn small color="success" class="flex items-center" @click="clickCreate">Create <span class="material-icons text-lg pl-2">add</span></ui-btn> <ui-btn small color="success" class="flex items-center" @click="clickCreate">{{ $strings.ButtonCreate }} <span class="material-icons text-lg pl-2">add</span></ui-btn>
</div> </div>
<div v-if="!notifications.length" class="flex justify-center text-center"> <div v-if="!notifications.length" class="flex justify-center text-center">
<p class="text-lg text-gray-200">No notifications</p> <p class="text-lg text-gray-200">{{ $strings.MessageNoNotifications }}</p>
</div> </div>
<template v-for="notification in notifications"> <template v-for="notification in notifications">
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" /> <cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
+40 -13
View File
@@ -2,23 +2,23 @@
<div class="w-full h-full"> <div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">Listening Sessions</h1> <h1 class="text-xl">{{ $strings.HeaderListeningSessions }}</h1>
</div> </div>
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" /> <ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
</div> </div>
<div v-if="listeningSessions.length" class="block max-w-full"> <div v-if="listeningSessions.length" class="block max-w-full">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th> <th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th> <th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th> <th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th> <th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">Listened</th> <th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">Last Time</th> <th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th> <th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr> </tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)"> <tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
@@ -55,7 +55,7 @@
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div> </div>
</div> </div>
<p v-else class="text-white text-opacity-50">No sessions yet...</p> <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
</div> </div>
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" /> <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
@@ -127,20 +127,47 @@ export default {
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) { if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode') this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
var queueItem = {}
if (session.episodeId) {
var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)
queueItem = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
episodeId: episode.id,
title: episode.title,
subtitle: libraryItem.media.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null
}
} else {
queueItem = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
episodeId: null,
title: libraryItem.media.metadata.title,
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
caption: '',
duration: libraryItem.media.duration || null,
coverPath: libraryItem.media.coverPath || null
}
}
const payload = { const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`, message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id, libraryItemId: libraryItem.id,
episodeId: session.episodeId || null, episodeId: session.episodeId || null,
startTime: session.currentTime startTime: session.currentTime,
queueItems: [queueItem]
}) })
} }
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
+7 -7
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<h1 class="text-xl">Stats for {{ username }}</h1> <h1 class="text-xl">{{ $strings.HeaderYourStats }}</h1>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex p-2"> <div class="flex p-2">
@@ -12,7 +12,7 @@
</svg> </svg>
<div class="px-3"> <div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p> <p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
</div> </div>
</div> </div>
@@ -22,7 +22,7 @@
</div> </div>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
</div> </div>
</div> </div>
@@ -32,7 +32,7 @@
</div> </div>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -40,11 +40,11 @@
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" /> <stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<div class="flex mb-4 items-center"> <div class="flex mb-4 items-center">
<h1 class="text-2xl font-book">Recent Sessions</h1> <h1 class="text-2xl font-book">{{ $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">View All</ui-btn> <ui-btn :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">No Listening Sessions</p> <p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
<template v-for="(item, index) in mostRecentListeningSessions"> <template v-for="(item, index) in mostRecentListeningSessions">
<div :key="item.id" class="w-full py-0.5"> <div :key="item.id" class="w-full py-0.5">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
+19 -19
View File
@@ -6,7 +6,7 @@
<div class="h-10 w-10 flex items-center justify-center"> <div class="h-10 w-10 flex items-center justify-center">
<span class="material-icons text-2xl">arrow_back</span> <span class="material-icons text-2xl">arrow_back</span>
</div> </div>
<p class="pl-1">All Users</p> <p class="pl-1">{{ $strings.LabelAllUsers }}</p>
</div> </div>
</nuxt-link> </nuxt-link>
<div class="flex items-center mb-2 mt-4 px-2 sm:px-0"> <div class="flex items-center mb-2 mt-4 px-2 sm:px-0">
@@ -22,22 +22,22 @@
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" /> <div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2"> <div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats</h1> <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderListeningStats }}</h1>
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p> <p class="text-sm text-gray-300">{{ listeningSessions.total }} {{ $strings.HeaderListeningSessions }}</p>
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">View All</ui-btn> <ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
</div> </div>
<p class="text-sm text-gray-300"> <p class="text-sm text-gray-300">
Total Time Listened:&nbsp; {{ $strings.LabelTotalTimeListened }}:&nbsp;
<span class="font-mono text-base">{{ listeningTimePretty }}</span> <span class="font-mono text-base">{{ listeningTimePretty }}</span>
</p> </p>
<p v-if="timeListenedToday" class="text-sm text-gray-300"> <p v-if="timeListenedToday" class="text-sm text-gray-300">
Time Listened Today:&nbsp; {{ $strings.LabelTimeListenedToday }}:&nbsp;
<span class="font-mono text-base">{{ $elapsedPrettyExtended(timeListenedToday) }}</span> <span class="font-mono text-base">{{ $elapsedPrettyExtended(timeListenedToday) }}</span>
</p> </p>
<div v-if="latestSession" class="mt-4"> <div v-if="latestSession" class="mt-4">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1> <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderLastListeningSession }}</h1>
<p class="text-sm text-gray-300"> <p class="text-sm text-gray-300">
<strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span> <strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span>
</p> </p>
@@ -45,21 +45,21 @@
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" /> <div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2"> <div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Saved Media Progress</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"> <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> <p class="text-error">User has media progress for {{ mediaProgressWithoutMedia.length }} items that no longer exist.</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">Purge Media Progress</ui-btn> <ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">{{ $strings.ButtonPurgeMediaProgress }}</ui-btn>
</div> </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">Item</th> <th class="w-16 text-left">{{ $strings.LabelItem }}</th>
<th class="text-left"></th> <th class="text-left"></th>
<th class="w-32">Progress</th> <th class="w-32">{{ $strings.LabelProgress }}</th>
<th class="w-40 hidden sm:table-cell">Started At</th> <th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
<th class="w-40 hidden sm:table-cell">Last Update</th> <th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr> </tr>
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'"> <tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
<td> <td>
@@ -90,7 +90,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<p v-else class="text-white text-opacity-50">Nothing listened to yet...</p> <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoMediaProgress }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -110,7 +110,7 @@ export default {
}, },
data() { data() {
return { return {
listeningSessions: [], listeningSessions: {},
listeningStats: {}, listeningStats: {},
purgingMediaProgress: false purgingMediaProgress: false
} }
@@ -147,8 +147,8 @@ export default {
return this.listeningStats.today || 0 return this.listeningStats.today || 0
}, },
latestSession() { latestSession() {
if (!this.listeningSessions.length) return null if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null
return this.listeningSessions[0] return this.listeningSessions.sessions[0]
} }
}, },
methods: { methods: {
@@ -159,11 +159,11 @@ export default {
this.listeningSessions = await this.$axios this.listeningSessions = await this.$axios
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`) .$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
.then((data) => { .then((data) => {
return data.sessions || [] return data || {}
}) })
.catch((err) => { .catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return {}
}) })
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => { this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
+38 -11
View File
@@ -6,7 +6,7 @@
<div class="h-10 w-10 flex items-center justify-center"> <div class="h-10 w-10 flex items-center justify-center">
<span class="material-icons text-2xl">arrow_back</span> <span class="material-icons text-2xl">arrow_back</span>
</div> </div>
<p class="pl-1">Back to User</p> <p class="pl-1">{{ $strings.LabelBackToUser }}</p>
</div> </div>
</nuxt-link> </nuxt-link>
<div class="flex items-center mb-2 mt-4 px-2 sm:px-0"> <div class="flex items-center mb-2 mt-4 px-2 sm:px-0">
@@ -17,16 +17,16 @@
<div class="w-full h-px bg-white bg-opacity-10 my-2" /> <div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2"> <div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1> <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
<div v-if="listeningSessions.length"> <div v-if="listeningSessions.length">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th> <th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th> <th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th> <th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">Listened</th> <th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">Last Time</th> <th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th> <th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr> </tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)"> <tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48"> <td class="py-1 max-w-48">
@@ -114,20 +114,47 @@ export default {
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) { if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode') this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
var queueItem = {}
if (session.episodeId) {
var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)
queueItem = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
episodeId: episode.id,
title: episode.title,
subtitle: libraryItem.media.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null
}
} else {
queueItem = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
episodeId: null,
title: libraryItem.media.metadata.title,
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
caption: '',
duration: libraryItem.media.duration || null,
coverPath: libraryItem.media.coverPath || null
}
}
const payload = { const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`, message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id, libraryItemId: libraryItem.id,
episodeId: session.episodeId || null, episodeId: session.episodeId || null,
startTime: session.currentTime startTime: session.currentTime,
queueItems: [queueItem]
}) })
} }
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
+67 -23
View File
@@ -42,7 +42,7 @@
<div v-if="narrator" class="flex py-0.5 mt-4"> <div v-if="narrator" class="flex py-0.5 mt-4">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
</div> </div>
<div> <div>
<template v-for="(narrator, index) in narrators"> <template v-for="(narrator, index) in narrators">
@@ -53,7 +53,7 @@
</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-32">
<span class="text-white text-opacity-60 uppercase text-sm">Publish Year</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
</div> </div>
<div> <div>
{{ publishedYear }} {{ publishedYear }}
@@ -61,7 +61,7 @@
</div> </div>
<div class="flex py-0.5" v-if="genres.length"> <div class="flex py-0.5" v-if="genres.length">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Genres</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
</div> </div>
<div> <div>
<template v-for="(genre, index) in genres"> <template v-for="(genre, index) in genres">
@@ -72,7 +72,7 @@
</div> </div>
<div v-if="tracks.length" class="flex py-0.5"> <div v-if="tracks.length" class="flex py-0.5">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Duration</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div> </div>
<div> <div>
{{ durationPretty }} {{ durationPretty }}
@@ -80,7 +80,7 @@
</div> </div>
<div class="flex py-0.5"> <div class="flex py-0.5">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Size</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
</div> </div>
<div> <div>
{{ sizePretty }} {{ sizePretty }}
@@ -100,7 +100,7 @@
<!-- 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">
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p> <p class="text-sm py-1">{{ $getString('MessageEpisodesQueuedForDownload', [episodeDownloadsQueued.length]) }}</p>
<span v-if="userIsAdminOrUp" class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span> <span v-if="userIsAdminOrUp" class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
</div> </div>
@@ -110,16 +110,16 @@
<div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0"> <div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center"> <div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
<p class="text-sm py-1 pl-4">Downloading episode "{{ episode.episodeDisplayTitle }}"</p> <p class="text-sm py-1 pl-4">{{ $strings.MessageDownloadingEpisode }} "{{ episode.episodeDisplayTitle }}"</p>
</div> </div>
</div> </div>
<!-- Progress --> <!-- Progress -->
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''"> <div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> <p v-if="progressPercent < 1" class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p> <p v-else class="text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> <p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, dateFormat) }}</p> <p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick"> <div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span> <span class="material-icons text-sm">close</span>
@@ -130,41 +130,45 @@
<div class="flex items-center justify-center md:justify-start pt-4"> <div class="flex items-center justify-center md:justify-start pt-4">
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem"> <ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> <span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ isStreaming ? 'Playing' : 'Play' }} {{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
</ui-btn> </ui-btn>
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2"> <ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span> <span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
{{ isMissing ? 'Missing' : 'Incomplete' }} {{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
</ui-btn> </ui-btn>
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_add'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
</ui-tooltip>
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> <ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span> <span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
Read {{ $strings.ButtonRead }}
</ui-btn> </ui-btn>
<ui-tooltip v-if="userCanUpdate" text="Edit" direction="top"> <ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top">
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" /> <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> <ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast && userCanUpdate" text="Collections" direction="top"> <ui-tooltip v-if="!isPodcast && userCanUpdate" :text="$strings.LabelCollections" direction="top">
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" /> <ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
</ui-tooltip> </ui-tooltip>
<!-- Only admin or root user can download new episodes --> <!-- Only admin or root user can download new episodes -->
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" text="Find Episodes" direction="top"> <ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" /> <ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="bookmarks.length" text="Your Bookmarks" direction="top"> <ui-tooltip v-if="bookmarks.length" :text="$strings.LabelYourBookmarks" direction="top">
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" /> <ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
</ui-tooltip> </ui-tooltip>
<!-- RSS feed --> <!-- RSS feed -->
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top"> <ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" /> <ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -398,6 +402,9 @@ export default {
isStreaming() { isStreaming() {
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
}, },
isQueued() {
return this.$store.getters['getIsMediaQueued'](this.libraryItemId)
},
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']
}, },
@@ -412,6 +419,10 @@ export default {
// If rss feed is open then show feed url to users otherwise just show to admins // If rss feed is open then show feed url to users otherwise just show to admins
return this.userIsAdminOrUp || this.rssFeedUrl return this.userIsAdminOrUp || this.rssFeedUrl
},
showQueueBtn() {
if (this.isPodcast || this.isVideo) return false
return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
} }
}, },
methods: { methods: {
@@ -507,12 +518,12 @@ 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(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
}) })
}, },
playItem(startTime = null) { playItem(startTime = null) {
@@ -536,6 +547,7 @@ export default {
if (!podcastProgress || !podcastProgress.isFinished) { if (!podcastProgress || !podcastProgress.isFinished) {
queueItems.push({ queueItems.push({
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.title, subtitle: this.title,
@@ -545,6 +557,18 @@ export default {
}) })
} }
} }
} else {
const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: null,
title: this.title,
subtitle: this.authors.map((au) => au.name).join(', '),
caption: '',
duration: this.duration || null,
coverPath: this.media.coverPath || null
}
queueItems.push(queueItem)
} }
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
@@ -582,7 +606,7 @@ export default {
}, },
collectionsClick() { collectionsClick() {
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setShowUserCollectionsModal', true) this.$store.commit('globals/setShowCollectionsModal', true)
}, },
clickRSSFeed() { clickRSSFeed() {
this.showRssFeedModal = true this.showRssFeedModal = true
@@ -615,6 +639,26 @@ export default {
console.log('RSS Feed Closed', data) console.log('RSS Feed Closed', data)
this.rssFeedUrl = null this.rssFeedUrl = null
} }
},
queueBtnClick() {
if (this.isQueued) {
// Remove from queue
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId })
} else {
// Add to queue
const queueItem = {
libraryItemId: this.libraryItemId,
libraryId: this.libraryId,
episodeId: null,
title: this.title,
subtitle: this.authors.map((au) => au.name).join(', '),
caption: '',
duration: this.duration || null,
coverPath: this.media.coverPath || null
}
this.$store.commit('addItemToQueue', queueItem)
}
} }
}, },
mounted() { mounted() {
@@ -1,11 +1,11 @@
<template> <template>
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"> <div class="page" :class="libraryItemIdStreaming ? 'streaming' : ''">
<app-book-shelf-toolbar page="recent-episodes" /> <app-book-shelf-toolbar page="recent-episodes" />
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative"> <div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
<div class="w-full max-w-3xl mx-auto py-4"> <div class="w-full max-w-3xl mx-auto py-4">
<p class="text-xl mb-2 font-semibold">Latest episodes</p> <p class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</p>
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">No podcasts found</p> <p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="(episode, index) in episodesMapped"> <template v-for="(episode, index) in episodesMapped">
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)"> <div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
@@ -18,14 +18,20 @@
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p> <p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)"> <div class="flex items-center">
<span v-if="episodeIdStreaming === episode.id" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span> <button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
<span v-else class="material-icons text-success">play_arrow</span> <span v-if="episodeIdStreaming === episode.id" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p> <span v-else class="material-icons text-success">play_arrow</span>
</button> <p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
</button>
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
<span class="material-icons-outlined">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
</button>
</div>
</div> </div>
<div v-if="episode.progress" class="absolute bottom-0 left-0 h-0.5 pointer-events-none bg-warning" :style="{ width: episode.progress.progress * 100 + '%' }" /> <div v-if="episode.progress" class="absolute bottom-0 left-0 h-0.5 pointer-events-none bg-warning" :style="{ width: episode.progress.progress * 100 + '%' }" />
</div> </div>
<div :key="index" v-if="index !== recentEpisodes.length" class="w-full h-px bg-white bg-opacity-10" /> <div :key="index" v-if="index !== recentEpisodes.length" class="w-full h-px bg-white bg-opacity-10" />
</template> </template>
@@ -63,9 +69,6 @@ export default {
} }
}, },
computed: { computed: {
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
@@ -78,6 +81,9 @@ export default {
streamIsPlaying() { streamIsPlaying() {
return this.$store.state.streamIsPlaying return this.$store.state.streamIsPlaying
}, },
isStreamingFromDifferentLibrary() {
return this.$store.getters['getIsStreamingFromDifferentLibrary']
},
episodesMapped() { episodesMapped() {
return this.recentEpisodes.map((ep) => { return this.recentEpisodes.map((ep) => {
return { return {
@@ -85,6 +91,16 @@ export default {
progress: this.$store.getters['user/getUserMediaProgress'](ep.libraryItemId, ep.id) progress: this.$store.getters['user/getUserMediaProgress'](ep.libraryItemId, ep.id)
} }
}) })
},
playerQueueItems() {
return this.$store.state.playerQueueItems || []
},
playerQueueEpisodeIdMap() {
const episodeIds = {}
this.playerQueueItems.forEach((i) => {
if (i.episodeId) episodeIds[i.episodeId] = true
})
return episodeIds
} }
}, },
methods: { methods: {
@@ -124,6 +140,7 @@ export default {
if (!episode.progress || !episode.isFinished) { if (!episode.progress || !episode.isFinished) {
queueItems.push({ queueItems.push({
libraryItemId: episode.libraryItemId, libraryItemId: episode.libraryItemId,
libraryId: episode.libraryId,
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: episode.podcast.metadata.title, subtitle: episode.podcast.metadata.title,
@@ -152,6 +169,25 @@ export default {
this.recentEpisodes = episodePayload.episodes || [] this.recentEpisodes = episodePayload.episodes || []
this.totalEpisodes = episodePayload.total this.totalEpisodes = episodePayload.total
this.currentPage = page this.currentPage = page
},
queueBtnClick(episode) {
if (this.playerQueueEpisodeIdMap[episode.id]) {
// Remove from queue
this.$store.commit('removeItemFromQueue', { libraryItemId: episode.libraryItemId, episodeId: episode.id })
} else {
// Add to queue
const queueItem = {
libraryItemId: episode.libraryItemId,
libraryId: episode.libraryId,
episodeId: episode.id,
title: episode.title,
subtitle: episode.podcast.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
duration: episode.duration || null,
coverPath: episode.podcast.coverPath || null
}
this.$store.commit('addItemToQueue', queueItem)
}
} }
}, },
mounted() { mounted() {

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