Merge branch 'advplyr:master' into master

This commit is contained in:
Hallo951 2022-11-09 09:50:09 +01:00 committed by GitHub
commit a7163f7a00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 854 additions and 720 deletions

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>

View File

@ -17,16 +17,16 @@
<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'" :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 {

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>

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">

View File

@ -1,164 +1,113 @@
<template> <template>
<div ref="card" :id="`book-card-${index}`" <div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
:style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }"
class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent
@mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<!-- When cover image does not fill --> <!-- When cover image does not fill -->
<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>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
:style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
'&nbsp;' <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
}}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{
displaySortLine
}}</p>
</div> </div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" <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">
: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> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div> </div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" <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">
: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> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
</div> </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" <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' }">
class="absolute top-0 left-0 w-full h-full flex items-center justify-center" <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
:style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}
</p>
</div> </div>
<!-- Cover Image --> <!-- Cover Image -->
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" <img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
:class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded"
:style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author --> <!-- Placeholder Cover Title & Author -->
<div v-if="!hasCover" <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' }">
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' }"> <p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
{{ titleCleaned }}</p> {{ titleCleaned }}
</p>
</div> </div>
</div> </div>
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" <div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
:style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }"> <p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75"
:style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
</div> </div>
</div> </div>
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) --> <!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" <div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b"
:class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }">
</div>
<!-- Finished progress bar for collapsed series --> <!-- Finished progress bar for collapsed series -->
<div v-else-if="booksInSeries && seriesIsFinished" <div v-else-if="booksInSeries && seriesIsFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success" :style="{ width: width * userProgressPercent + 'px' }"></div>
class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success"
:style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library --> <!-- Overlay is not shown if collapsing series in library -->
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" <div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block"
:class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none"> <div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
@click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span> <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
</div> </div>
</div> </div>
<div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none"> <div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
@click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span> <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
</div> </div>
</div> </div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0"
:style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div> </div>
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" <div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
:style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
@click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''"
:style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' :
'radio_button_unchecked'
}}</span>
</div> </div>
<!-- More Menu Icon --> <!-- More Menu Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" <div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }"
@click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span> <span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div> </div>
</div> </div>
<!-- Processing/loading spinner overlay --> <!-- Processing/loading spinner overlay -->
<div v-if="processing" <div v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
<widgets-loading-spinner size="la-lg" /> <widgets-loading-spinner size="la-lg" />
</div> </div>
<!-- Series name overlay --> <!-- Series name overlay -->
<div v-if="booksInSeries && libraryItem && isHovering" <div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center"
:style="{ padding: sizeMultiplier + 'rem' }">
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p> <p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
</div> </div>
<!-- Error widget --> <!-- Error widget -->
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10"> <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300"> <span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
<span class="material-icons text-red-100 pr-1"
:style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div> </div>
</ui-tooltip> </ui-tooltip>
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" <div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
:style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span> <span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
</div> </div>
<!-- Series sequence --> <!-- Series sequence -->
<div v-if="seriesSequence && !isHovering && !isSelectionMode" <div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10"
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div> </div>
<!-- Podcast Episode # --> <!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" <div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10"
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
</div> </div>
<!-- Podcast Num Episodes --> <!-- Podcast Num Episodes -->
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" <div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center"
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div> </div>
</div> </div>
@ -446,17 +395,17 @@ 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
}) })
} }
return items return items
@ -467,42 +416,42 @@ 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
}) })
} }
return items return items
@ -594,7 +543,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) {
@ -638,12 +587,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() {

View File

@ -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>

View File

@ -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)}`">

View File

@ -1,27 +1,19 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer"
aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
@click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</button> </button>
<ul v-show="showMenu" <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">
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" <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)">
: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">
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span> <span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
</div> </div>
<span v-if="item.value === selected" <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</li> </li>
@ -37,55 +29,32 @@ export default {
descending: Boolean descending: Boolean
}, },
data() { data() {
const bookItems = [
{
text: 'Title',
value: 'media.metadata.title'
},
{
text: 'Author (First Last)',
value: 'media.metadata.authorName'
},
{
text: 'Author (Last, First)',
value: 'media.metadata.authorNameLF'
},
{
text: 'Published Year',
value: 'media.metadata.publishedYear'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Size',
value: 'size'
},
{
text: 'Duration',
value: 'media.duration'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
}
]
const seriesItems = [...bookItems, {
text: 'Sequence',
value: 'sequence'
}]
return { return {
showMenu: false, showMenu: false
bookItems: bookItems, }
seriesItems: seriesItems, },
podcastItems: [ 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'
@ -115,27 +84,55 @@ export default {
value: 'mtimeMs' value: 'mtimeMs'
} }
] ]
}
},
computed: {
selected: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}, },
selectedDesc: { bookItems() {
get() { return [
return this.descending {
}, text: this.$strings.LabelTitle,
set(val) { value: 'media.metadata.title'
this.$emit('update:descending', val) },
} {
text: 'Author (First Last)',
value: 'media.metadata.authorName'
},
{
text: 'Author (Last, First)',
value: 'media.metadata.authorNameLF'
},
{
text: 'Published Year',
value: 'media.metadata.publishedYear'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Size',
value: 'size'
},
{
text: 'Duration',
value: 'media.duration'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
}
]
}, },
isPodcast() { seriesItems() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' return [
...this.bookItems,
{
text: 'Sequence',
value: 'sequence'
}
]
}, },
selectItems() { selectItems() {
let items = null let items = null
@ -147,7 +144,7 @@ export default {
items = this.bookItems items = this.bookItems
} }
if (!items.some(i => i.value === this.selected)) { if (!items.some((i) => i.value === this.selected)) {
this.selected = items[0].value this.selected = items[0].value
this.selectedDesc = !this.defaultsToAsc(items[0].value) this.selectedDesc = !this.defaultsToAsc(items[0].value)
} }
@ -178,13 +175,7 @@ export default {
this.$nextTick(() => this.$emit('change', val)) this.$nextTick(() => this.$emit('change', val))
}, },
defaultsToAsc(val) { defaultsToAsc(val) {
return ( return val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF' || val == 'sequence'
val == 'media.metadata.title' ||
val == 'media.metadata.author' ||
val == 'media.metadata.authorName' ||
val == 'media.metadata.authorNameLF' ||
val == 'sequence'
);
} }
} }
} }

View File

@ -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,6 +147,22 @@ 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
}, },
@ -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
} }
}) })

View File

@ -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>

View File

@ -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
} }
@ -115,7 +107,7 @@ export default {
doBatchQuickMatch() { doBatchQuickMatch() {
if (!this.selectedBookIds.length) return if (!this.selectedBookIds.length) return
if (this.processing) return if (this.processing) return
this.processing = true this.processing = true
this.$store.commit('setProcessingBatch', true) this.$store.commit('setProcessingBatch', true)
this.$axios this.$axios
@ -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

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)
}) })

View File

@ -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)
}) })
} }
}, },

View File

@ -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)
}) })
} }
}, },

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>

View File

@ -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="!showBatchUserCollectionModal" 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>
@ -65,7 +65,7 @@ export default {
}, },
title() { title() {
if (this.showBatchUserCollectionModal) { if (this.showBatchUserCollectionModal) {
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 : ''
}, },
@ -124,12 +124,12 @@ export default {
.$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 +138,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
}) })
} }

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')
} }

View File

@ -179,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

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">

View File

@ -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()

View File

@ -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

View File

@ -50,7 +50,7 @@
</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>

View File

@ -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)
}, },

View File

@ -192,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
}) })
@ -211,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
@ -223,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
}) })

View File

@ -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'
} }
] ]

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)

View File

@ -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}`)
}) })

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

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() {

View File

@ -22,7 +22,7 @@
<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'"> <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>

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>

View File

@ -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>

View File

@ -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">
@ -88,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
}) })
} }
@ -119,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) {
@ -139,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
}) })

View File

@ -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>

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>

View File

@ -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
}) })
} }

View File

@ -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'">
@ -153,12 +153,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 +168,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
}) })
} }

View File

@ -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
}) })
} }

View File

@ -20,7 +20,7 @@
<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"> <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>
@ -159,12 +159,12 @@ 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() {

View File

@ -1,21 +1,21 @@
<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" />
</template> </template>
@ -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
}, },
@ -141,12 +145,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
}) })

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>

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>

View File

@ -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>

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 []
}) })

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>

View File

@ -12,7 +12,7 @@
<template v-if="!showAdvancedView"> <template v-if="!showAdvancedView">
<ui-dropdown v-model="selectedInterval" @input="updateCron" :label="$strings.LabelInterval" :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="$string.LabelWeekdaysToRun" :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="$strings.LabelHour" 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" />

View File

@ -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>

View File

@ -23,7 +23,7 @@
<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">* {{ $strings.NoteChangeRootPassword }}</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">{{ $strings.ButtonChangePasswordSubmit }}</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>

View File

@ -134,18 +134,18 @@
<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">{{ $strings.LabelStart }}</div> <div class="w-24 px-2">{{ $strings.LabelStart }}</div>
@ -164,11 +164,11 @@
<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">
@ -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) => {

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'
}
} }
} }
} }

View File

@ -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">

View File

@ -16,7 +16,7 @@
</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>
@ -24,7 +24,7 @@
<div class="py-4"> <div class="py-4">
<widgets-item-slider :items="libraryItems" :bookshelf-view="$constants.BookshelfView.AUTHOR"> <widgets-item-slider :items="libraryItems" :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>
@ -34,7 +34,7 @@
<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>

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) => {

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" />

View File

@ -72,9 +72,14 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="py-2">
<p class="pr-4">{{ $strings.LabelSettingsDateFormat }}</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-48" @input="(val) => updateSettingsKey('dateFormat', val)" />
</div>
<div class="py-2">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguageDefaultServer }}</p>
<ui-dropdown v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-48" @input="updateServerLanguage" />
</div> </div>
</div> </div>
@ -321,6 +326,10 @@ 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.$setLanguageCode(val)
this.updateSettingsKey('language', val)
},
updateSettingsKey(key, val) { updateSettingsKey(key, val) {
this.updateServerSettings({ this.updateServerSettings({
[key]: val [key]: val

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,41 @@
<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-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="Edit" 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>
@ -507,12 +507,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) {

View File

@ -4,8 +4,8 @@
<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" />
@ -25,7 +25,7 @@
</button> </button>
</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>

View File

@ -6,14 +6,14 @@
<div class="w-full max-w-4xl mx-auto flex"> <div class="w-full max-w-4xl mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
<ui-btn type="submit" :disabled="processing" class="hidden md:block">Submit</ui-btn> <ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>Submit</ui-btn> <ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
</form> </form>
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input> <ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload">{{ $strings.ButtonUploadOPMLFile }}</ui-file-input>
</div> </div>
<div class="w-full max-w-3xl mx-auto py-4"> <div class="w-full max-w-3xl mx-auto py-4">
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p> <p v-if="termSearched && !results.length && !processing" class="text-center text-xl">{{ $strings.MessageNoPodcastsFound }}</p>
<template v-for="podcast in results"> <template v-for="podcast in results">
<div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)"> <div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)">
<div class="w-20 min-w-20 h-20 md:w-24 md:min-w-24 md:h-24 bg-primary"> <div class="w-20 min-w-20 h-20 md:w-24 md:min-w-24 md:h-24 bg-primary">
@ -23,7 +23,7 @@
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a> <a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p> <p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p> <p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p> <p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} {{ $strings.HeaderEpisodes }}</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -3,7 +3,7 @@
<app-book-shelf-toolbar is-home page="search" :search-query="query" /> <app-book-shelf-toolbar is-home page="search" :search-query="query" />
<app-book-shelf-categorized v-if="hasResults" ref="bookshelf" search :results="results" /> <app-book-shelf-categorized v-if="hasResults" ref="bookshelf" search :results="results" />
<div v-else class="w-full py-16"> <div v-else class="w-full py-16">
<p class="text-xl text-center">No Search results for "{{ query }}"</p> <p class="text-xl text-center">{{ $getString('MessageNoSearchResultsFor', [query]) }}</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
<div class="w-full h-screen bg-bg"> <div class="w-full h-screen bg-bg">
<div class="w-full flex h-full items-center justify-center"> <div class="w-full flex h-full items-center justify-center">
<div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4">
<p class="text-center text-lg font-semibold">Server could not be reached</p> <p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p>
</div> </div>
<div v-else-if="showInitScreen" class="w-full max-w-lg px-4 md:px-8 pb-8 pt-4"> <div v-else-if="showInitScreen" class="w-full max-w-lg px-4 md:px-8 pb-8 pt-4">
<p class="text-3xl text-white text-center mb-4">Initial Server Setup</p> <p class="text-3xl text-white text-center mb-4">Initial Server Setup</p>
@ -19,22 +19,22 @@
<ui-text-input-with-label v-model="MetadataPath" label="Metadata Path" disabled class="w-full mb-3 text-sm" /> <ui-text-input-with-label v-model="MetadataPath" label="Metadata Path" disabled class="w-full mb-3 text-sm" />
<div class="w-full flex justify-end py-3"> <div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Initializing...' : 'Submit' }}</ui-btn> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Initializing...' : $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">Login</p> <p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">Username</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" /> <ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" />
<label class="text-xs text-gray-300 uppercase">Password</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" /> <ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" />
<div class="w-full flex justify-end py-3"> <div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : 'Submit' }}</ui-btn> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div> </div>
</form> </form>
</div> </div>
@ -128,6 +128,7 @@ export default {
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source) this.$store.commit('setSource', Source)
this.$store.commit('feeds/setFeeds', feeds) this.$store.commit('feeds/setFeeds', feeds)
this.$setServerLanguageCode(serverSettings.language)
if (serverSettings.chromecastEnabled) { if (serverSettings.chromecastEnabled) {
console.log('Chromecast enabled import script') console.log('Chromecast enabled import script')
@ -189,6 +190,7 @@ export default {
this.processing = false this.processing = false
this.isInit = res.isInit this.isInit = res.isInit
this.showInitScreen = !res.isInit this.showInitScreen = !res.isInit
this.$setServerLanguageCode(res.language)
if (this.showInitScreen) { if (this.showInitScreen) {
this.ConfigPath = res.ConfigPath || '' this.ConfigPath = res.ConfigPath || ''
this.MetadataPath = res.MetadataPath || '' this.MetadataPath = res.MetadataPath || ''

View File

@ -1,19 +1,31 @@
import Vue from "vue" import Vue from "vue"
import enUsStrings from '../strings/en-us.json'
import { supplant } from './utils'
const defaultCode = 'en-us' const defaultCode = 'en-us'
function supplant(str, subs) { const languageCodeMap = {
// source: http://crockford.com/javascript/remedial.html 'en-us': 'English',
return str.replace(/{([^{}]*)}/g, 'es': 'Español',
function (a, b) { 'it': 'Italiano',
var r = subs[b] 'pl': 'Polski',
return typeof r === 'string' || typeof r === 'number' ? r : a 'zh-cn': '汉语 (简化字)'
} }
) Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
return {
text: languageCodeMap[code],
value: code
}
})
Vue.prototype.$languageCodes = {
default: defaultCode,
current: defaultCode,
local: null,
server: null
} }
Vue.prototype.$i18nCode = '' Vue.prototype.$strings = { ...enUsStrings }
Vue.prototype.$strings = {}
Vue.prototype.$getString = (key, subs) => { Vue.prototype.$getString = (key, subs) => {
if (!Vue.prototype.$strings[key]) return '' if (!Vue.prototype.$strings[key]) return ''
if (subs && Array.isArray(subs) && subs.length) { if (subs && Array.isArray(subs) && subs.length) {
@ -22,7 +34,9 @@ Vue.prototype.$getString = (key, subs) => {
return Vue.prototype.$strings[key] return Vue.prototype.$strings[key]
} }
var translations = {} var translations = {
[defaultCode]: enUsStrings
}
function loadTranslationStrings(code) { function loadTranslationStrings(code) {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -30,35 +44,64 @@ function loadTranslationStrings(code) {
resolve(fileContents.default) resolve(fileContents.default)
}).catch((error) => { }).catch((error) => {
console.error('Failed to load i18n strings', code, error) console.error('Failed to load i18n strings', code, error)
resolve({}) resolve(null)
}) })
}) })
} }
async function loadi18n(code) { async function loadi18n(code) {
if (Vue.prototype.$i18nCode == code) { if (!code) return false
if (Vue.prototype.$languageCodes.current == code) {
// already set // already set
return return false
} }
const currentCode = Vue.prototype.$i18nCode
const strings = translations[code] || await loadTranslationStrings(code) const strings = translations[code] || await loadTranslationStrings(code)
if (!strings) {
console.warn(`Invalid lang code ${code}`)
return false
}
translations[code] = strings translations[code] = strings
Vue.prototype.$i18nCode = code Vue.prototype.$languageCodes.current = code
localStorage.setItem('lang', code)
if (!currentCode) {
// first load
Vue.prototype.$strings = strings
return
}
for (const key in Vue.prototype.$strings) { for (const key in Vue.prototype.$strings) {
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key] Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
} }
console.log('i18n strings=', Vue.prototype.$strings) console.log('i18n strings=', Vue.prototype.$strings)
return true
} }
Vue.prototype.$i18nUpdate = loadi18n Vue.prototype.$setLanguageCode = loadi18n
// Set the servers default language code, does not override users local language code
Vue.prototype.$setServerLanguageCode = (code) => {
if (!code) return
if (!languageCodeMap[code]) {
console.warn('invalid server language in', code)
} else {
Vue.prototype.$languageCodes.server = code
if (!Vue.prototype.$languageCodes.local && code !== defaultCode) {
loadi18n(code)
}
}
}
// Initialize with language code in localStorage if valid
async function initialize() {
const localLanguage = localStorage.getItem('lang')
if (!localLanguage) return
if (!languageCodeMap[localLanguage]) {
console.warn('Invalid local language code', localLanguage)
localStorage.setItem('lang', defaultCode)
} else if (localLanguage !== defaultCode) {
Vue.prototype.$languageCodes.local = localLanguage
loadi18n(localLanguage)
}
}
initialize()
loadi18n(defaultCode)

View File

@ -134,4 +134,14 @@ Vue.prototype.$parseCronExpression = (expression) => {
return { return {
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}` description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}`
} }
}
export function supplant(str, subs) {
// source: http://crockford.com/javascript/remedial.html
return str.replace(/{([^{}]*)}/g,
function (a, b) {
var r = subs[b]
return typeof r === 'string' || typeof r === 'number' ? r : a
}
)
} }

View File

@ -1,19 +1,25 @@
{ {
"ButtonAdd": "Add", "ButtonAdd": "Add",
"ButtonAddChapters": "Add Chapters", "ButtonAddChapters": "Add Chapters",
"ButtonAddPodcasts": "Add Podcasts",
"ButtonAddYourFirstLibrary": "Add your first library", "ButtonAddYourFirstLibrary": "Add your first library",
"ButtonApply": "Apply",
"ButtonApplyChapters": "Apply Chapters", "ButtonApplyChapters": "Apply Chapters",
"ButtonAuthors": "Authors", "ButtonAuthors": "Authors",
"ButtonBrowseForFolder": "Browse for Folder", "ButtonBrowseForFolder": "Browse for Folder",
"ButtonCancel": "Cancel", "ButtonCancel": "Cancel",
"ButtonChangePasswordSubmit": "Submit", "ButtonCancelEncode": "Cancel Encode",
"ButtonChangeRootPassword": "Change Root Password", "ButtonChangeRootPassword": "Change Root Password",
"ButtonCheckAndDownloadNewEpisodes": "Check & Download New Episodes",
"ButtonChooseAFolder": "Choose a folder", "ButtonChooseAFolder": "Choose a folder",
"ButtonChooseFiles": "Choose files", "ButtonChooseFiles": "Choose files",
"ButtonCloseFeed": "Close Feed",
"ButtonCollections": "Collections", "ButtonCollections": "Collections",
"ButtonCreate": "Create", "ButtonCreate": "Create",
"ButtonCreateBackup": "Create Backup", "ButtonCreateBackup": "Create Backup",
"ButtonDelete": "Delete",
"ButtonEditChapters": "Edit Chapters", "ButtonEditChapters": "Edit Chapters",
"ButtonEditPodcast": "Edit Podcast",
"ButtonForceReScan": "Force Re-Scan", "ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path", "ButtonFullPath": "Full Path",
"ButtonHide": "Hide", "ButtonHide": "Hide",
@ -23,34 +29,45 @@
"ButtonLogout": "Logout", "ButtonLogout": "Logout",
"ButtonLookup": "Lookup", "ButtonLookup": "Lookup",
"ButtonLibrary": "Library", "ButtonLibrary": "Library",
"ButtonManageTracks": "Manage Tracks",
"ButtonMapChapterTitles": "Map Chapter Titles",
"ButtonMatchAllAuthors": "Match All Authors", "ButtonMatchAllAuthors": "Match All Authors",
"ButtonMatchBooks": "Match Books", "ButtonMatchBooks": "Match Books",
"ButtonMapChapterTitles": "Map Chapter Titles",
"ButtonNevermind": "Nevermind", "ButtonNevermind": "Nevermind",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Open Feed",
"ButtonOpenManager": "Open Manager", "ButtonOpenManager": "Open Manager",
"ButtonPlay": "Play",
"ButtonPlaying": "Playing",
"ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonPurgeMediaProgress": "Purge Media Progress",
"ButtonQuickMatch": "Quick Match", "ButtonQuickMatch": "Quick Match",
"ButtonRead": "Read",
"ButtonRemove": "Remove", "ButtonRemove": "Remove",
"ButtonRemoveAll": "Remove All", "ButtonRemoveAll": "Remove All",
"ButtonRemoveAllLibraryItems": "Remove All Library Items", "ButtonRemoveAllLibraryItems": "Remove All Library Items",
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
"ButtonReScan": "Re-Scan", "ButtonReScan": "Re-Scan",
"ButtonReset": "Reset", "ButtonReset": "Reset",
"ButtonRestore": "Restore", "ButtonRestore": "Restore",
"ButtonSave": "Save", "ButtonSave": "Save",
"ButtonSaveAndClose": "Save & Close", "ButtonSaveAndClose": "Save & Close",
"ButtonSaveTracklist": "Save Tracklist",
"ButtonScan": "Scan", "ButtonScan": "Scan",
"ButtonSearch": "Search", "ButtonSearch": "Search",
"ButtonSelectFolderPath": "Select Folder Path", "ButtonSelectFolderPath": "Select Folder Path",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonShiftTimes": "Shift Times", "ButtonShiftTimes": "Shift Times",
"ButtonShow": "Show", "ButtonShow": "Show",
"ButtonStartM4BEncode": "Start M4B Encode",
"ButtonStartMetadataEmbed": "Start Metadata Embed",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup", "ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover", "ButtonUploadCover": "Upload Cover",
"ButtonUploadOPMLFile": "Upload OPML File",
"ButtonViewAll": "View All", "ButtonViewAll": "View All",
"ButtonYes": "Yes", "ButtonYes": "Yes",
"HeaderAccount": "Account", "HeaderAccount": "Account",
@ -63,6 +80,7 @@
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
"HeaderChooseAFolder": "Choose a Folder", "HeaderChooseAFolder": "Choose a Folder",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderEpisodes": "Episodes", "HeaderEpisodes": "Episodes",
@ -71,27 +89,38 @@
"HeaderIgnoredFiles": "Ignored Files", "HeaderIgnoredFiles": "Ignored Files",
"HeaderItemFiles": "Item Files", "HeaderItemFiles": "Item Files",
"HeaderLastListeningSession": "Last Listening Session", "HeaderLastListeningSession": "Last Listening Session",
"HeaderLatestEpisodes": "Latest episodes",
"HeaderLibraries": "Libraries", "HeaderLibraries": "Libraries",
"HeaderLibraryFiles": "Library Files", "HeaderLibraryFiles": "Library Files",
"HeaderLibraryStats": "Library Stats", "HeaderLibraryStats": "Library Stats",
"HeaderListeningSessions": "Listening Sessions", "HeaderListeningSessions": "Listening Sessions",
"HeaderListeningStats": "Listening Stats", "HeaderListeningStats": "Listening Stats",
"HeaderLogin": "Login",
"HeaderLogs": "Logs", "HeaderLogs": "Logs",
"HeaderMatch": "Match", "HeaderMatch": "Match",
"HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account", "HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library", "HeaderNewLibrary": "New Library",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOtherFiles": "Other Files", "HeaderOtherFiles": "Other Files",
"HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderPermissions": "Permissions", "HeaderPermissions": "Permissions",
"HeaderPodcastsToAdd": "Podcasts to Add",
"HeaderPreviewCover": "Preview Cover", "HeaderPreviewCover": "Preview Cover",
"HeaderRemoveEpisode": "Remove Episode",
"HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Set Backup Schedule",
"HeaderSettings": "Settings", "HeaderSettings": "Settings",
"HeaderSettingsDisplay": "Display", "HeaderSettingsDisplay": "Display",
"HeaderSettingsExperimental": "Experimental Features", "HeaderSettingsExperimental": "Experimental Features",
"HeaderSettingsGeneral": "General", "HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner", "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Sleep Timer",
"HeaderStatsLongestItems": "Longest Items (hrs)", "HeaderStatsLongestItems": "Longest Items (hrs)",
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)", "HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
"HeaderStatsRecentSessions": "Recent Sessions", "HeaderStatsRecentSessions": "Recent Sessions",
@ -99,15 +128,22 @@
"HeaderStatsTop5Genres": "Top 5 Genres", "HeaderStatsTop5Genres": "Top 5 Genres",
"HeaderTools": "Tools", "HeaderTools": "Tools",
"HeaderUpdateAccount": "Update Account", "HeaderUpdateAccount": "Update Account",
"HeaderUpdateAuthor": "Update Author",
"HeaderUpdateDetails": "Update Details", "HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library", "HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users", "HeaderUsers": "Users",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAccountType": "Account Type", "LabelAccountType": "Account Type",
"LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Guest",
"LabelAccountTypeUser": "User",
"LabelActivity": "Activity", "LabelActivity": "Activity",
"LabelAddToCollection": "Add to Collection",
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAllUsers": "All Users", "LabelAllUsers": "All Users",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthors": "Authors", "LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
@ -117,48 +153,85 @@
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChaptersFound": "chapters found",
"LabelChapterTitle": "Chapter Title",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
"LabelCollections": "Collections", "LabelCollections": "Collections",
"LabelComplete": "Complete",
"LabelConfirmPassword": "Confirm Password", "LabelConfirmPassword": "Confirm Password",
"LabelContinueListening": "Continue Listening",
"LabelContinueSeries": "Continue Series",
"LabelCover": "Cover", "LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL", "LabelCoverImageURL": "Cover Image URL",
"LabelCreatedAt": "Created At", "LabelCreatedAt": "Created At",
"LabelCronExpression": "Cron Expression", "LabelCronExpression": "Cron Expression",
"LabelCurrent": "Current",
"LabelCurrently": "Currently:", "LabelCurrently": "Currently:",
"LabelDatetime": "Datetime", "LabelDatetime": "Datetime",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDeselectAll": "Deselect All",
"LabelDevice": "Device",
"LabelDeviceInfo": "Device Info", "LabelDeviceInfo": "Device Info",
"LabelDirectory": "Directory", "LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelDurationFound": "Duration found:",
"LabelEdit": "Edit",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"LabelEnd": "End", "LabelEnd": "End",
"LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFile": "File", "LabelFile": "File",
"LabelFilename": "Filename", "LabelFilename": "Filename",
"LabelFilterByUser": "Filter by User", "LabelFilterByUser": "Filter by User",
"LabelFindEpisodes": "Find Episodes",
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist",
"LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress",
"LabelInterval": "Interval", "LabelInterval": "Interval",
"LabelInvalidParts": "Invalid Parts",
"LabelItem": "Item", "LabelItem": "Item",
"LabelLanguage": "Language", "LabelLanguage": "Language",
"LabelLanguageDefaultServer": "Default Server Language",
"LabelLastSeen": "Last Seen", "LabelLastSeen": "Last Seen",
"LabelLastTime": "Last Time", "LabelLastTime": "Last Time",
"LabelLastUpdate": "Last Update", "LabelLastUpdate": "Last Update",
"LabelLess": "Less",
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User", "LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
"LabelLibrary": "Library", "LabelLibrary": "Library",
"LabelLibraryItem": "Library Item",
"LabelLibraryName": "Library Name", "LabelLibraryName": "Library Name",
"LabelLimit": "Limit",
"LabelListenAgain": "Listen Again",
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
"LabelMarkSeries": "Mark Series", "LabelMarkSeries": "Mark Series",
"LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts",
"LabelMore": "More",
"LabelName": "Name", "LabelName": "Name",
"LabelNarrators": "Narrators", "LabelNarrators": "Narrators",
"LabelNew": "New",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNewPassword": "New Password", "LabelNewPassword": "New Password",
"LabelNotes": "Notes",
"LabelNotFinished": "Not Finished", "LabelNotFinished": "Not Finished",
"LabelNotificationEvent": "Notification Event", "LabelNotificationEvent": "Notification Event",
"LabelNotificationAppriseURL": "Apprise URL(s)", "LabelNotificationAppriseURL": "Apprise URL(s)",
@ -169,6 +242,7 @@
"LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times", "LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
"LabelNotificationsMaxQueueSize": "Max queue size for notification events", "LabelNotificationsMaxQueueSize": "Max queue size for notification events",
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.", "LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
"LabelOpenRSSFeed": "Open RSS Feed",
"LabelPassword": "Password", "LabelPassword": "Password",
"LabelPath": "Path", "LabelPath": "Path",
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries", "LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
@ -178,18 +252,26 @@
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload", "LabelPermissionsUpload": "Can Upload",
"LabelPhotoPathURL": "Photo Path/URL",
"LabelPlayMethod": "Play Method", "LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelProvider": "Provider", "LabelProvider": "Provider",
"LabelPubDate": "Pub Date",
"LabelPublisher": "Publisher", "LabelPublisher": "Publisher",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL",
"LabelSearchTerm": "Search Term", "LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title", "LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN", "LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
@ -227,9 +309,12 @@
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
"LabelSettingsSquareBookCovers": "User square book covers", "LabelSettingsSquareBookCovers": "User square book covers",
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers", "LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
"LabelShowAll": "Show All",
"LabelSize": "Size", "LabelSize": "Size",
"LabelStart": "Start", "LabelStart": "Start",
"LabelStarted": "Started",
"LabelStartedAt": "Started At", "LabelStartedAt": "Started At",
"LabelStartTime": "Start Time",
"LabelStatsAudioTracks": "Audio Tracks", "LabelStatsAudioTracks": "Audio Tracks",
"LabelStatsAuthors": "Authors", "LabelStatsAuthors": "Authors",
"LabelStatsBestDay": "Best Day", "LabelStatsBestDay": "Best Day",
@ -250,29 +335,74 @@
"LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
"LabelTimeListenedToday": "Time Listened Today", "LabelTimeListenedToday": "Time Listened Today",
"LabelTimeRemaining": "{0} remaining",
"LabelTimeToShift": "Time to shift in seconds", "LabelTimeToShift": "Time to shift in seconds",
"LabelTitle": "Title", "LabelTitle": "Title",
"LabelTotalTimeListened": "Total Time Listened", "LabelTotalTimeListened": "Total Time Listened",
"LabelTrackFromFilename": "Track from Filename",
"LabelTrackFromMetadata": "Track from Metadata",
"LabelType": "Type", "LabelType": "Type",
"LabelUnknown": "Unknown",
"LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
"LabelUpdatedAt": "Updated At",
"LabelUpdateDetails": "Update Details",
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track",
"LabelUser": "User", "LabelUser": "User",
"LabelUsername": "Username", "LabelUsername": "Username",
"LabelValue": "Value",
"LabelVersion": "Version",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks",
"LabelYourProgress": "Your Progress",
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in",
"MessageBackupsNote": "Backups do not include any files stored in your library folders.", "MessageBackupsNote": "Backups do not include any files stored in your library folders.",
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
"MessageCheckingCron": "Checking cron...", "MessageCheckingCron": "Checking cron...",
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!",
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageFeedURLWillBe": "Feed URL will be {0}",
"MessageFetching": "Fetching...",
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.", "MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
"MessageImportantNotice": "Important Notice!", "MessageImportantNotice": "Important Notice!",
"MessageInsertChapterBelow": "Insert chapter below", "MessageInsertChapterBelow": "Insert chapter below",
"MessageItemsSelected": "{0} Items Selected",
"MessageJoinUsOn": "Join us on", "MessageJoinUsOn": "Join us on",
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
"MessageLoading": "Loading...", "MessageLoading": "Loading...",
"MessageLoadingFolders": "Loading folders...", "MessageLoadingFolders": "Loading folders...",
"MessageM4BFailed": "M4B Failed!",
"MessageM4BFinished": "M4B Finished!",
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps", "MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
"MessageMarkAsFinished": "Mark as Finished",
"MessageMarkAsNotFinished": "Mark as Not Finished",
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.", "MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
"MessageNoAudioTracks": "No audio tracks",
"MessageNoAuthors": "No Authors", "MessageNoAuthors": "No Authors",
"MessageNoBackups": "No Backups",
"MessageNoBookmarks": "No Bookmarks",
"MessageNoChapters": "No Chapters", "MessageNoChapters": "No Chapters",
"MessageNoCollections": "No Collections",
"MessageNoCoversFound": "No Covers Found", "MessageNoCoversFound": "No Covers Found",
"MessageNoDescription": "No description",
"MessageNoEpisodeMatchesFound": "No episode matches found",
"MessageNoEpisodes": "No Episodes",
"MessageNoFoldersAvailable": "No Folders Available", "MessageNoFoldersAvailable": "No Folders Available",
"MessageNoGenres": "No Genres", "MessageNoGenres": "No Genres",
"MessageNoItems": "No Items", "MessageNoItems": "No Items",
@ -281,29 +411,98 @@
"MessageNoLogs": "No Logs", "MessageNoLogs": "No Logs",
"MessageNoMediaProgress": "No Media Progress", "MessageNoMediaProgress": "No Media Progress",
"MessageNoNotifications": "No Notifications", "MessageNoNotifications": "No Notifications",
"MessageNoPodcastsFound": "No podcasts found",
"MessageNoResults": "No Results", "MessageNoResults": "No Results",
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
"MessageNoUpdateNecessary": "No update necessary",
"MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?", "MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
"MessageRemoveEpisodes": "Remove {0} episode(s)",
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?", "MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
"MessageThinking": "Thinking...",
"MessageUploaderItemFailed": "Failed to upload", "MessageUploaderItemFailed": "Failed to upload",
"MessageUploaderItemSuccess": "Successfully Uploaded!", "MessageUploaderItemSuccess": "Successfully Uploaded!",
"MessageUploading": "Uploading...", "MessageUploading": "Uploading...",
"MessageValidCronExpression": "Valid cron expression", "MessageValidCronExpression": "Valid cron expression",
"MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings", "MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
"MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found",
"MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found",
"NoteChangeRootPassword": "Root user is the only user that can have an empty password", "NoteChangeRootPassword": "Root user is the only user that can have an empty password",
"NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
"NoteFolderPicker": "Note: folders already mapped will not be shown", "NoteFolderPicker": "Note: folders already mapped will not be shown",
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.", "NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",
"NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.", "NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.",
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.", "NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
"PlaceholderNewCollection": "New collection name",
"PlaceholderNewFolderPath": "New folder path", "PlaceholderNewFolderPath": "New folder path",
"PlaceholderSearch": "Search..", "PlaceholderSearch": "Search..",
"ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated",
"ToastAuthorImageRemoveFailed": "Failed to remove image",
"ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorUpdateFailed": "Failed to update author",
"ToastAuthorUpdateMerged": "Author merged",
"ToastAuthorUpdateSuccess": "Author updated",
"ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
"ToastBackupCreateFailed": "Failed to create backup",
"ToastBackupCreateSuccess": "Backup created",
"ToastBackupDeleteFailed": "Failed to delete backup",
"ToastBackupDeleteSuccess": "Backup deleted",
"ToastBackupRestoreFailed": "Failed to restore backup",
"ToastBackupUploadFailed": "Failed to upload backup",
"ToastBackupUploadSuccess": "Backup uploaded",
"ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added",
"ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkRemoveSuccess": "Bookmark removed",
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastBookmarkUpdateSuccess": "Bookmark updated",
"ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection",
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
"ToastCollectionRemoveFailed": "Failed to remove collection",
"ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateFailed": "Failed to update collection",
"ToastCollectionUpdateSuccess": "Collection updated",
"ToastItemCoverUpdateFailed": "Failed to update item cover",
"ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDetailsUpdateFailed": "Failed to update item details",
"ToastItemDetailsUpdateSuccess": "Item details updated",
"ToastItemDetailsUpdateUnneeded": "No updates needed for item details",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
"ToastLibraryCreateFailed": "Failed to create library",
"ToastLibraryCreateSuccess": "Library \"{0}\" created",
"ToastLibraryDeleteFailed": "Failed to delete library",
"ToastLibraryDeleteSuccess": "Library deleted",
"ToastLibraryScanFailedToStart": "Failed to start scan",
"ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateFailed": "Failed to update library",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
"ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastSessionDeleteFailed": "Failed to delete session",
"ToastSessionDeleteSuccess": "Session deleted",
"ToastUserDeleteFailed": "Failed to delete user",
"ToastUserDeleteSuccess": "User deleted",
"WeekdayFriday": "Friday", "WeekdayFriday": "Friday",
"WeekdayMonday": "Monday", "WeekdayMonday": "Monday",
"WeekdaySaturday": "Saturday", "WeekdaySaturday": "Saturday",

View File

@ -51,4 +51,4 @@
"MessageReportBugsAndContribute": "报告错误, 请求新功能, 做贡献请点击", "MessageReportBugsAndContribute": "报告错误, 请求新功能, 做贡献请点击",
"NoteChangeRootPassword": "Root 用户是唯一可以有空密码的用户", "NoteChangeRootPassword": "Root 用户是唯一可以有空密码的用户",
"SearchPlaceholder": "搜索.." "SearchPlaceholder": "搜索.."
} }

View File

@ -254,7 +254,8 @@ class Server {
// status check for client to see if server has been initialized // status check for client to see if server has been initialized
// server has been initialized if a root user exists // server has been initialized if a root user exists
const payload = { const payload = {
isInit: this.db.hasRootUser isInit: this.db.hasRootUser,
language: this.db.serverSettings.language
} }
if (!payload.isInit) { if (!payload.isInit) {
payload.ConfigPath = global.ConfigPath payload.ConfigPath = global.ConfigPath

View File

@ -53,6 +53,7 @@ class ServerSettings {
this.chromecastEnabled = false this.chromecastEnabled = false
this.enableEReader = false this.enableEReader = false
this.dateFormat = 'MM/dd/yyyy' this.dateFormat = 'MM/dd/yyyy'
this.language = 'en-us'
this.logLevel = Logger.logLevel this.logLevel = Logger.logLevel
@ -102,6 +103,7 @@ class ServerSettings {
this.chromecastEnabled = !!settings.chromecastEnabled this.chromecastEnabled = !!settings.chromecastEnabled
this.enableEReader = !!settings.enableEReader this.enableEReader = !!settings.enableEReader
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
this.language = settings.language || 'en-us'
this.logLevel = settings.logLevel || Logger.logLevel this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null this.version = settings.version || null
@ -153,6 +155,7 @@ class ServerSettings {
chromecastEnabled: this.chromecastEnabled, chromecastEnabled: this.chromecastEnabled,
enableEReader: this.enableEReader, enableEReader: this.enableEReader,
dateFormat: this.dateFormat, dateFormat: this.dateFormat,
language: this.language,
logLevel: this.logLevel, logLevel: this.logLevel,
version: this.version version: this.version
} }

View File

@ -320,6 +320,7 @@ module.exports = {
{ {
id: 'continue-listening', id: 'continue-listening',
label: 'Continue Listening', label: 'Continue Listening',
labelStringKey: 'LabelContinueListening',
type: isPodcastLibrary ? 'episode' : mediaType, type: isPodcastLibrary ? 'episode' : mediaType,
entities: [], entities: [],
category: 'recentlyListened' category: 'recentlyListened'
@ -327,6 +328,7 @@ module.exports = {
{ {
id: 'continue-series', id: 'continue-series',
label: 'Continue Series', label: 'Continue Series',
labelStringKey: 'LabelContinueSeries',
type: mediaType, type: mediaType,
entities: [], entities: [],
category: 'continueSeries' category: 'continueSeries'
@ -334,6 +336,7 @@ module.exports = {
{ {
id: 'recently-added', id: 'recently-added',
label: 'Recently Added', label: 'Recently Added',
labelStringKey: 'LabelRecentlyAdded',
type: mediaType, type: mediaType,
entities: [], entities: [],
category: 'newestItems' category: 'newestItems'
@ -341,6 +344,7 @@ module.exports = {
{ {
id: 'listen-again', id: 'listen-again',
label: 'Listen Again', label: 'Listen Again',
labelStringKey: 'LabelListenAgain',
type: isPodcastLibrary ? 'episode' : mediaType, type: isPodcastLibrary ? 'episode' : mediaType,
entities: [], entities: [],
category: 'recentlyFinished' category: 'recentlyFinished'
@ -348,6 +352,7 @@ module.exports = {
{ {
id: 'recent-series', id: 'recent-series',
label: 'Recent Series', label: 'Recent Series',
labelStringKey: 'LabelRecentSeries',
type: 'series', type: 'series',
entities: [], entities: [],
category: 'newestSeries' category: 'newestSeries'
@ -355,6 +360,7 @@ module.exports = {
{ {
id: 'newest-authors', id: 'newest-authors',
label: 'Newest Authors', label: 'Newest Authors',
labelStringKey: 'LabelNewestAuthors',
type: 'authors', type: 'authors',
entities: [], entities: [],
category: 'newestAuthors' category: 'newestAuthors'
@ -362,6 +368,7 @@ module.exports = {
{ {
id: 'episodes-recently-added', id: 'episodes-recently-added',
label: 'Newest Episodes', label: 'Newest Episodes',
labelStringKey: 'LabelNewestEpisodes',
type: 'episode', type: 'episode',
entities: [], entities: [],
category: 'newestEpisodes' category: 'newestEpisodes'