Update:More localization strings #1103

This commit is contained in:
advplyr 2022-11-07 18:27:17 -06:00
parent eb463a2958
commit faac6f677a
46 changed files with 772 additions and 527 deletions

View File

@ -2,19 +2,19 @@
<div class="w-full h-20 md:h-10 relative">
<div class="flex md:hidden h-10 items-center">
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Home</p>
<p class="text-sm">{{ $strings.ButtonHome }}</p>
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Library</p>
<p class="text-sm">{{ $strings.ButtonLibrary }}</p>
</nuxt-link>
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Series</p>
<p class="text-sm">{{ $strings.ButtonSeries }}</p>
</nuxt-link>
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Collections</p>
<p class="text-sm">{{ $strings.ButtonCollections }}</p>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Search</p>
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
</nuxt-link>
</div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
@ -28,42 +28,37 @@
<span class="font-mono">{{ numShowing }}</span>
</div>
<div class="flex-grow" />
<ui-checkbox v-model="settings.collapseBookSeries" label="Collapse Series" checkbox-bg="bg"
check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4"
@click="markSeriesFinished">
<ui-checkbox v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4" @click="markSeriesFinished">
<div class="h-5 w-5">
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="rgb(63, 181, 68)">
<path
d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
</div>
<span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span>
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
</ui-btn>
</div>
<div class="flex-grow hidden sm:inline-block" />
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
<controls-library-filter-select v-if="isLibraryPage" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
<controls-library-sort-select v-if="isLibraryPage" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
<controls-library-filter-select v-if="isSeriesPage" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
<controls-sort-select v-if="isSeriesPage" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">Remove All {{ numShowing }} {{ entityName }}</ui-btn>
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
</template>
<template v-else-if="page === 'search'">
<div class="flex-grow" />
<p>Search results for "{{ searchQuery }}"</p>
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
<div class="flex-grow" />
</template>
<template v-else-if="page === 'authors'">
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">Match All Authors</ui-btn>
<ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
</template>
</div>
</div>
@ -150,10 +145,10 @@ export default {
return this.totalEntities
},
entityName() {
if (this.isPodcastLibrary) return 'Podcasts'
if (!this.page) return 'Books'
if (this.isSeriesPage) return 'Series'
if (this.isCollectionsPage) return 'Collections'
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
if (!this.page) return this.$strings.LabelBooks
if (this.isSeriesPage) return this.$strings.LabelSeries
if (this.isCollectionsPage) return this.$strings.LabelCollections
return ''
},
seriesName() {

View File

@ -18,7 +18,7 @@
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
</div>
</template>
@ -47,7 +47,7 @@ export default {
return [
{
id: 'config-stats',
title: 'Your Stats',
title: this.$strings.HeaderYourStats,
path: '/config/stats'
}
]
@ -55,37 +55,37 @@ export default {
const configRoutes = [
{
id: 'config',
title: 'Settings',
title: this.$strings.HeaderSettings,
path: '/config'
},
{
id: 'config-libraries',
title: 'Libraries',
title: this.$strings.HeaderLibraries,
path: '/config/libraries'
},
{
id: 'config-users',
title: 'Users',
title: this.$strings.HeaderUsers,
path: '/config/users'
},
{
id: 'config-sessions',
title: 'Listening Sessions',
title: this.$strings.HeaderListeningSessions,
path: '/config/sessions'
},
{
id: 'config-backups',
title: 'Backups',
title: this.$strings.HeaderBackups,
path: '/config/backups'
},
{
id: 'config-log',
title: 'Logs',
title: this.$strings.HeaderLogs,
path: '/config/log'
},
{
id: 'config-notifications',
title: 'Notifications',
title: this.$strings.HeaderNotifications,
path: '/config/notifications'
}
]
@ -93,12 +93,12 @@ export default {
if (this.currentLibraryId) {
configRoutes.push({
id: 'config-library-stats',
title: 'Library Stats',
title: this.$strings.HeaderLibraryStats,
path: '/config/library-stats'
})
configRoutes.push({
id: 'config-stats',
title: 'Your Stats',
title: this.$strings.HeaderYourStats,
path: '/config/stats'
})
}
@ -146,7 +146,7 @@ export default {
}
},
methods: {
clickChangelog(){
clickChangelog() {
this.showChangelogModal = true
},
clickOutside() {
@ -155,7 +155,7 @@ export default {
},
closeDrawer() {
this.$emit('update:isOpen', false)
},
}
}
}
</script>

View File

@ -15,41 +15,41 @@
<div class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" label="Title" @input="titleUpdated" />
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" label="Author" />
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<div v-else class="w-full">
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p>
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
</div>
</div>
</div>
<div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" label="Series" note="(optional)" />
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
</div>
<div class="w-1/2 px-2">
<div class="w-full">
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p>
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
</div>
</div>
</div>
<tables-uploaded-files-table :files="item.itemFiles" title="Item Files" class="mt-8" />
<tables-uploaded-files-table v-if="item.otherFiles.length" title="Other Files" :files="item.otherFiles" />
<tables-uploaded-files-table v-if="item.ignoredFiles.length" title="Ignored Files" :files="item.ignoredFiles" />
<tables-uploaded-files-table :files="item.itemFiles" :title="$strings.HeaderItemFiles" class="mt-8" />
<tables-uploaded-files-table v-if="item.otherFiles.length" :title="$strings.HeaderOtherFiles" :files="item.otherFiles" />
<tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template>
<widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">Successfully Uploaded!</p>
<p class="text-base">{{ $strings.MessageUploaderItemSuccess }}</p>
</widgets-alert>
<widgets-alert v-if="uploadFailed" type="error">
<p class="text-base">Failed to upload</p>
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert>
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator text="Uploading..." />
<ui-loading-indicator :text="$strings.MessageUploading" />
</div>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="sm:w-80 w-full relative">
<form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.SearchPlaceholder" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>

View File

@ -10,28 +10,28 @@
<div class="w-full p-8">
<div class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.username" label="Username" />
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" />
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
</div>
</div>
<div v-show="!isEditingRoot" class="flex py-2">
<div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
</div>
<div class="flex-grow" />
<div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p>
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
</div>
</div>
<div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4">
<p class="text-lg mb-2 font-semibold">Permissions</p>
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Download</p>
<p>{{ $strings.LabelPermissionsDownload }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.download" />
@ -40,7 +40,7 @@
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Update</p>
<p>{{ $strings.LabelPermissionsUpdate }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.update" />
@ -49,7 +49,7 @@
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Delete</p>
<p>{{ $strings.LabelPermissionsDelete }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.delete" />
@ -58,7 +58,7 @@
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Upload</p>
<p>{{ $strings.LabelPermissionsUpload }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.upload" />
@ -67,7 +67,7 @@
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Access Explicit Content</p>
<p>{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" />
@ -76,7 +76,7 @@
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p>Can Access All Libraries</p>
<p>{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
@ -84,26 +84,26 @@
</div>
<div v-if="!newUser.permissions.accessAllLibraries" class="my-4">
<ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" label="Libraries Accessible to User" />
<ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" :label="$strings.LabelLibrariesAccessibleToUser" />
</div>
<div class="flex items-cen~ter my-2 max-w-md">
<div class="w-1/2">
<p>Can Access All Tags</p>
<p>{{ $strings.LabelPermissionsAccessAllTags }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" />
</div>
</div>
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" label="Tags Accessible to User" />
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" />
</div>
</div>
<div class="flex pt-4 px-2">
<ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn>
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" />
<ui-btn color="success" type="submit">Submit</ui-btn>
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
@ -165,7 +165,7 @@ export default {
return this.$store.state.user.user
},
title() {
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
},
isEditingRoot() {
return this.account && this.account.type === 'root'

View File

@ -2,7 +2,7 @@
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="font-book text-3xl text-white truncate">Collection</p>
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
</div>
</template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
@ -14,15 +14,15 @@
<!-- <ui-btn type="button" @click="showImageUploader = true">Upload</ui-btn> -->
</div>
<div class="flex-grow px-4">
<ui-text-input-with-label v-model="newCollectionName" label="Name" class="mb-2" />
<ui-text-input-with-label v-model="newCollectionName" :label="$strings.LabelName" class="mb-2" />
<ui-textarea-with-label v-model="newCollectionDescription" label="Description" />
<ui-textarea-with-label v-model="newCollectionDescription" :label="$strings.LabelDescription" />
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">Remove</ui-btn>
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
<div class="flex-grow" />
<ui-btn color="success" type="submit">Save</ui-btn>
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</form>
</template>

View File

@ -8,14 +8,14 @@
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
<div class="flex">
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" label="Series Name" />
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" />
</div>
<div class="w-24 sm:w-28 md:w-40 p-1">
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" label="Sequence" />
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
</div>
</div>
<div class="flex justify-end mt-2 p-1">
<ui-btn type="submit">Save</ui-btn>
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</div>
</form>

View File

@ -31,55 +31,7 @@ export default {
processing: false,
libraryItem: null,
availableHeight: 0,
marginTop: 0,
tabs: [
{
id: 'details',
title: 'Details',
component: 'modals-item-tabs-details'
},
{
id: 'cover',
title: 'Cover',
component: 'modals-item-tabs-cover'
},
{
id: 'chapters',
title: 'Chapters',
component: 'modals-item-tabs-chapters',
mediaType: 'book'
},
{
id: 'episodes',
title: 'Episodes',
component: 'modals-item-tabs-episodes',
mediaType: 'podcast'
},
{
id: 'files',
title: 'Files',
component: 'modals-item-tabs-files'
},
{
id: 'match',
title: 'Match',
component: 'modals-item-tabs-match'
},
{
id: 'tools',
title: 'Tools',
component: 'modals-item-tabs-tools',
mediaType: 'book',
admin: true
},
{
id: 'schedule',
title: 'Schedule',
component: 'modals-item-tabs-schedule',
mediaType: 'podcast',
admin: true
}
]
marginTop: 0
}
},
watch: {
@ -122,6 +74,56 @@ export default {
this.$store.commit('setEditModalTab', val)
}
},
tabs() {
return [
{
id: 'details',
title: this.$strings.HeaderDetails,
component: 'modals-item-tabs-details'
},
{
id: 'cover',
title: this.$strings.HeaderCover,
component: 'modals-item-tabs-cover'
},
{
id: 'chapters',
title: this.$strings.HeaderChapters,
component: 'modals-item-tabs-chapters',
mediaType: 'book'
},
{
id: 'episodes',
title: this.$strings.HeaderEpisodes,
component: 'modals-item-tabs-episodes',
mediaType: 'podcast'
},
{
id: 'files',
title: this.$strings.HeaderFiles,
component: 'modals-item-tabs-files'
},
{
id: 'match',
title: this.$strings.HeaderMatch,
component: 'modals-item-tabs-match'
},
{
id: 'tools',
title: this.$strings.HeaderTools,
component: 'modals-item-tabs-tools',
mediaType: 'book',
admin: true
},
{
id: 'schedule',
title: this.$strings.HeaderSchedule,
component: 'modals-item-tabs-schedule',
mediaType: 'podcast',
admin: true
}
]
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},

View File

@ -3,8 +3,8 @@
<div class="w-full mb-4">
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open />
<div v-if="!chapters.length" class="py-4 text-center">
<p class="mb-8 text-xl">No Chapters</p>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">Add Chapters</ui-btn>
<p class="mb-8 text-xl">{{ $strings.MessageNoChapters }}</p>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">{{ $strings.ButtonAddChapters }}</ui-btn>
</div>
</div>
</div>

View File

@ -14,11 +14,14 @@
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
<div class="flex items-center">
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected"><span class="hidden md:inline-block">Upload Cover</span><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input>
<ui-file-input ref="fileInput" @change="fileUploadSelected"
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input
>
</div>
<form @submit.prevent="submitForm" class="flex flex-grow">
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">Update</ui-btn>
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">{{ $strings.ButtonSave }}</ui-btn>
</form>
</div>
@ -26,7 +29,7 @@
<div class="flex items-center justify-center py-2">
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
<div class="flex-grow" />
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? 'Hide' : 'Show' }}</ui-btn>
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
</div>
<div v-if="showLocalCovers" class="flex items-center justify-center">
@ -44,19 +47,19 @@
<form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 h-20">
<div class="w-40 px-1">
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div>
<div class="w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div>
<div v-show="provider != 'itunes'" class="w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div>
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div>
</form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">No Covers Found</p>
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
@ -65,14 +68,14 @@
</div>
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
<p class="text-lg">Preview Cover</p>
<p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
<div class="flex justify-center py-4">
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="absolute bottom-0 right-0 flex py-4 px-5">
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">Clear</ui-btn>
<ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">Upload</ui-btn>
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">{{ $strings.ButtonUpload }}</ui-btn>
</div>
</div>
</div>
@ -125,9 +128,9 @@ export default {
return this.$store.state.scanners.providers
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
else if (this.provider == 'itunes') return 'Search Term'
return 'Search Title'
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
else if (this.provider == 'itunes') return this.$strings.LabelSearchTerm
return this.$strings.LabelSearchTitle
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']

View File

@ -7,22 +7,24 @@
<div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
<div class="flex items-center px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">{{ $strings.ButtonRemove }}</ui-btn>
<ui-icon-btn bg-color="error" icon="delete" class="md:hidden" :size="7" icon-font-size="1rem" @click.stop.prevent="removeItem" />
<div class="flex-grow" />
<ui-tooltip :disabled="!!quickMatching" :text="`Populate empty ${mediaType} details & cover with first ${mediaType} result from '${libraryProvider}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.`" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
<ui-tooltip :disabled="!!quickMatching" :text="$getString('MessageQuickMatchDescription', [libraryProvider])" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
</ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
</ui-tooltip>
<ui-btn @click="save" class="mx-2 hidden md:block">Save</ui-btn>
<ui-btn @click="saveAndClose">Save<span class="hidden md:inline-block">&nbsp;& Close</span></ui-btn>
<!-- desktop -->
<ui-btn @click="save" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
<ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
<!-- mobile -->
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
</div>
</div>
</div>

View File

@ -3,22 +3,22 @@
<form @submit.prevent="submitSearch">
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
<div class="w-36 px-1">
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div>
<div class="flex-grow md:w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div>
<div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div>
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div>
</form>
<div v-show="processing" class="flex h-full items-center justify-center">
<p>Loading...</p>
<p>{{ $strings.MessageLoading }}</p>
</div>
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
<p>No Results</p>
<p>{{ $strings.MessageNoResults }}</p>
</div>
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
<template v-for="(res, index) in searchResults">
@ -30,13 +30,13 @@
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<p class="text-xl pl-3">Update Book Details</p>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div>
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
<form @submit.prevent="submitMatchUpdate">
<div v-if="selectedMatchOrig.cover" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly label="Cover" class="flex-grow mx-4" />
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
<div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16">
<a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary">
<img :src="selectedMatch.cover" class="h-full w-full object-contain" />
@ -46,50 +46,50 @@
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.title || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.title || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.subtitle || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.subtitle || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.author" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.authorName || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.authorName || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" label="Narrator" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.narratorName || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p>
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publisher || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publisher || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" label="Published Year" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.publishedYear || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear || '' }}</p>
</div>
</div>
@ -97,42 +97,42 @@
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.seriesName || '' }}</p>
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.seriesName || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" label="Genres" />
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.genres.join(', ') }}</p>
<ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" label="Tags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ media.tags.join(', ') }}</p>
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.language" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" label="Language" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.language || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.language || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.isbn || '' }}</p>
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.isbn || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.asin || '' }}</p>
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.asin || '' }}</p>
</div>
</div>
@ -140,33 +140,33 @@
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesId || '' }}</p>
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesId || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.feedUrl || '' }}</p>
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.feedUrl || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.itunesPageUrl || '' }}</p>
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesPageUrl || '' }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" label="Release Date" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.releaseDate || '' }}</p>
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
</div>
</div>
<div class="flex items-center justify-end py-2">
<ui-btn color="success" type="submit">Update</ui-btn>
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</form>
</div>
@ -258,9 +258,9 @@ export default {
return this.$store.state.scanners.providers
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
else if (this.provider == 'itunes') return 'Search Term'
return 'Search Title'
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
else if (this.provider == 'itunes') return this.$strings.LabelSearchTerm
return this.$strings.LabelSearchTitle
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">Audiobook File Management Tools</p>
<p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- Merge to m4b -->
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
@ -12,7 +12,7 @@
<div class="flex-grow" />
<div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
>Open Manager
>{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span>
</ui-btn>
</div>
@ -43,7 +43,7 @@
<div class="flex-grow" />
<div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
>Open Manager
>{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span>
</ui-btn>
</div>

View File

@ -3,21 +3,21 @@
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
<div class="flex flex-wrap md:flex-nowrap -mx-1">
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="mediaType" :items="mediaTypes" label="Media Type" :disabled="!isNew" small @input="changedMediaType" />
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
</div>
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
<ui-text-input-with-label v-model="name" label="Library Name" @blur="nameBlurred" />
<ui-text-input-with-label v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
</div>
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
<ui-media-icon-picker v-model="icon" @input="iconChanged" />
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
</div>
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small @input="formUpdated" />
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelMetadataProvider" small @input="formUpdated" />
</div>
</div>
<div class="w-full py-4">
<p class="px-1 text-sm font-semibold">Folders</p>
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
@ -25,10 +25,10 @@
</div>
<div class="flex py-1 px-2 items-center w-full">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" @blur="newFolderInputBlurred" />
<ui-editable-text v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
</div>
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">Browse for Folder</ui-btn>
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
</div>
</div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />

View File

@ -36,23 +36,6 @@ export default {
return {
processing: false,
selectedTab: 'details',
tabs: [
{
id: 'details',
title: 'Details',
component: 'modals-libraries-edit-library'
},
{
id: 'settings',
title: 'Settings',
component: 'modals-libraries-library-settings'
},
{
id: 'schedule',
title: 'Schedule',
component: 'modals-libraries-schedule-scan'
}
],
libraryCopy: null
}
},
@ -66,10 +49,29 @@ export default {
}
},
title() {
return this.library ? 'Update Library' : 'New Library'
return this.library ? this.$strings.HeaderUpdateLibrary : this.$strings.HeaderNewLibrary
},
buttonText() {
return this.library ? 'Update Library' : 'Create New Library'
return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate
},
tabs() {
return [
{
id: 'details',
title: this.$strings.HeaderDetails,
component: 'modals-libraries-edit-library'
},
{
id: 'settings',
title: this.$strings.HeaderSettings,
component: 'modals-libraries-library-settings'
},
{
id: 'schedule',
title: this.$strings.HeaderSchedule,
component: 'modals-libraries-schedule-scan'
}
]
},
tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab)

View File

@ -2,7 +2,7 @@
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
<div class="flex items-center py-1 mb-2">
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">Choose a Folder</p>
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div>
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
@ -27,16 +27,16 @@
</div>
</div>
<div v-else-if="loadingFolders" class="py-12 text-center">
<p>Loading folders...</p>
<p>{{ $strings.MessageLoadingFolders }}</p>
</div>
<div v-else class="py-12 text-center max-w-sm mx-auto">
<p class="text-lg mb-2">No Folders Available</p>
<p class="text-gray-300 mb-2">Note: folders already mapped will not be shown</p>
<p v-if="isDebian" class="text-red-400">Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.</p>
<p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p>
<p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p>
<p v-if="isDebian" class="text-red-400">{{ $strings.NoteFolderPickerDebian }}</p>
</div>
<div class="w-full py-2">
<ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">Select Folder Path</ui-btn>
<ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">{{ $strings.ButtonSelectFolderPath }}</ui-btn>
</div>
</div>
</template>

View File

@ -2,9 +2,9 @@
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center py-2">
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
<ui-tooltip :text="tooltips.coverAspectRatio">
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
<p class="pl-4 text-base">
Use square book covers
{{ $strings.LabelSettingsSquareBookCovers }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -13,20 +13,20 @@
<div class="flex items-center">
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
<ui-toggle-switch v-else disabled :value="false" />
<p class="pl-4 text-base">Disable folder watcher for library</p>
<p class="pl-4 text-base">{{ $strings.LabelSettingsDisableWatcherForLibrary }}</p>
</div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
</div>
<div v-if="mediaType == 'book'" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
<p class="pl-4 text-base">Skip matching books that already have an ASIN</p>
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
</div>
</div>
<div v-if="mediaType == 'book'" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
<p class="pl-4 text-base">Skip matching books that already have an ISBN</p>
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
</div>
</div>
</div>
@ -47,10 +47,7 @@ export default {
useSquareBookCovers: false,
disableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
tooltips: {
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
}
skipMatchingMediaWithIsbn: false
}
},
computed: {

View File

@ -1,8 +1,8 @@
<template>
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center justify-between mb-4">
<p class="text-base md:text-xl font-semibold">Schedule Automatic Library Scans</p>
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
<p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleLibraryScans }}</p>
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
</div>
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
</div>

View File

@ -8,23 +8,25 @@
<form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12">
<ui-dropdown v-model="newNotification.eventName" label="Notification Event" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
<ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
<ui-multi-select v-model="newNotification.urls" label="Apprise URL(s)" class="mb-2" />
<ui-multi-select v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
<ui-text-input-with-label v-model="newNotification.titleTemplate" label="Title Template" class="mb-2" />
<ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
<ui-textarea-with-label v-model="newNotification.bodyTemplate" label="Body Template" :rows="4" class="mb-2" />
<ui-textarea-with-label v-model="newNotification.bodyTemplate" :label="$strings.LabelNotificationBodyTemplate" :rows="4" class="mb-2" />
<p v-if="availableVariables" class="text-sm text-gray-300"><strong>Available variables:</strong> {{ availableVariables.join(', ') }}</p>
<p v-if="availableVariables" class="text-sm text-gray-300">
<strong>{{ $strings.LabelNotificationAvailableVariables }}:</strong> {{ availableVariables.join(', ') }}
</p>
<div class="flex items-center pt-4">
<div class="flex items-center">
<ui-toggle-switch v-model="newNotification.enabled" />
<p class="text-lg pl-2">Enabled</p>
<p class="text-lg pl-2">{{ $strings.LabelEnable }}</p>
</div>
<div class="flex-grow" />
<ui-btn color="success" type="submit">Submit</ui-btn>
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>

View File

@ -5,10 +5,10 @@
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-lg mb-8 mt-2 px-1" v-html="message" />
<div class="flex px-1 items-center">
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">Cancel</ui-btn>
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">{{ $strings.ButtonCancel }}</ui-btn>
<div class="flex-grow" />
<ui-btn v-if="isYesNo" color="success" @click="confirm">Yes</ui-btn>
<ui-btn v-else color="primary" @click="confirm">Ok</ui-btn>
<ui-btn v-if="isYesNo" color="success" @click="confirm">{{ $strings.ButtonYes }}</ui-btn>
<ui-btn v-else color="primary" @click="confirm">{{ $strings.ButtonOk }}</ui-btn>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">
@ -34,24 +34,24 @@
</div>
<div class="flex justify-between pt-12">
<div>
<p class="text-sm text-center">Week Listening</p>
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Daily Average</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Best Day</p>
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Days</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-sm text-center">in a row</p>
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@
</svg>
<div class="px-2">
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items in Library</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
</div>
</div>
@ -14,7 +14,7 @@
<span class="material-icons text-7xl">show_chart</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Overall {{ useOverallHours ? 'Hours' : 'Days' }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsOverall }} {{ useOverallHours ? $strings.LabelStatsHours : $strings.LabelStatsDays }}</p>
</div>
</div>
@ -24,7 +24,7 @@
</svg>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Authors</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
</div>
</div>
@ -32,7 +32,7 @@
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Size ({{ totalSizeMod }})</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
</div>
</div>
@ -40,7 +40,7 @@
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Audio Tracks</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
</div>
</div>
</div>

View File

@ -1,16 +1,16 @@
<template>
<div class="text-center mt-4">
<div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">Upload Backup</ui-file-input>
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" />
<ui-btn :loading="isBackingUp" @click="clickCreateBackup">Create Backup</ui-btn>
<ui-btn :loading="isBackingUp" @click="clickCreateBackup">{{ $strings.ButtonCreateBackup }}</ui-btn>
</div>
<div class="relative">
<table id="backups">
<tr>
<th>File</th>
<th class="hidden sm:table-cell w-32 md:w-56">Datetime</th>
<th class="hidden sm:table-cell w-20 md:w-28">Size</th>
<th>{{ $strings.LabelFile }}</th>
<th class="hidden sm:table-cell w-32 md:w-56">{{ $strings.LabelDatetime }}</th>
<th class="hidden sm:table-cell w-20 md:w-28">{{ $strings.LabelSize }}</th>
<th class="w-36"></th>
</tr>
<tr v-for="backup in backups" :key="backup.id" :class="!backup.serverVersion ? 'bg-error bg-opacity-10' : ''">
@ -21,7 +21,7 @@
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td>
<div class="w-full flex flex-row items-center justify-center">
<ui-btn v-if="backup.serverVersion" small color="primary" @click="applyBackup(backup)">Restore</ui-btn>
<ui-btn v-if="backup.serverVersion" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
@ -43,16 +43,14 @@
<prompt-dialog v-model="showConfirmApply" :width="675">
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-error text-lg font-semibold">Important Notice!</p>
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p>
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.</p>
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p>
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?</p>
<div class="flex px-1 items-center">
<ui-btn color="primary" @click="showConfirmApply = false">Nevermind</ui-btn>
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
<div class="flex-grow" />
<ui-btn color="success" @click="confirm">Apply Backup</ui-btn>
<ui-btn color="success" @click="confirm">{{ $strings.ButtonRestore }}</ui-btn>
</div>
</div>
</prompt-dialog>

View File

@ -1,10 +1,10 @@
<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">Chapters</p>
<p class="pr-4">{{ $strings.HeaderChapters }}</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span>
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">Edit Chapters</ui-btn>
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
@ -13,9 +13,9 @@
<table class="text-sm tracksTable" v-show="expanded || keepOpen">
<tr class="font-book">
<th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">Title</th>
<th class="text-center">Start</th>
<th class="text-center">End</th>
<th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center">{{ $strings.LabelStart }}</th>
<th class="text-center">{{ $strings.LabelEnd }}</th>
</tr>
<tr v-for="chapter in chapters" :key="chapter.id">
<td class="text-left">

View File

@ -1,12 +1,12 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
<p class="pr-2 md:pr-4">Library Files</p>
<p class="pr-2 md:pr-4">{{ $strings.HeaderLibraryFiles }}</p>
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-sm font-mono">{{ files.length }}</span>
</div>
<div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
@ -15,10 +15,10 @@
<div class="w-full" v-show="showFiles">
<table class="text-sm tracksTable">
<tr class="font-book">
<th class="text-left px-4">Path</th>
<th class="text-left w-24 min-w-24">Size</th>
<th class="text-left px-4 w-24">Filetype</th>
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
<th v-if="userCanDownload && !isMissing" class="text-center w-20">{{ $strings.LabelDownload }}</th>
</tr>
<template v-for="file in files">
<tr :key="file.path">

View File

@ -12,9 +12,9 @@
<div class="w-full" v-show="expand">
<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">Type</th>
<th class="text-left">{{ $strings.LabelFilename }}</th>
<th class="text-left">{{ $strings.LabelSize }}</th>
<th class="text-left">{{ $strings.LabelType }}</th>
</tr>
<template v-for="file in files">
<tr :key="file.path">

View File

@ -6,15 +6,15 @@
<span class="material-icons" style="font-size: 1.4rem">add</span>
</div>
</div>
<!-- <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> -->
<div class="text-center">
<table id="accounts">
<tr>
<th>Username</th>
<th class="w-20">Type</th>
<th class="hidden lg:table-cell">Activity</th>
<th class="w-32 hidden sm:table-cell">Last Seen</th>
<th class="w-32 hidden sm:table-cell">Created</th>
<th>{{ $strings.LabelUsername }}</th>
<th class="w-20">{{ $strings.LabelAccountType }}</th>
<th class="hidden lg:table-cell">{{ $strings.LabelActivity }}</th>
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelLastSeen }}</th>
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th>
</tr>
<tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : 'bg-error bg-opacity-20'" @click="$router.push(`/config/users/${user.id}`)">
@ -88,7 +88,7 @@ export default {
methods: {
deleteUserClick(user) {
if (this.isDeletingUser) return
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
this.isDeletingUser = true
this.$axios
.$delete(`/api/users/${user.id}`)

View File

@ -14,12 +14,16 @@
</template>
</draggable>
<div v-if="!libraries.length" class="pb-4">
<ui-btn @click="clickAddLibrary">Add your first library</ui-btn>
<ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
</div>
<p v-if="libraries.length" class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p>
<p v-if="libraries.length" class="text-xs mt-4 text-gray-200">
*<strong>{{ $strings.ButtonForceReScan }}</strong> {{ $strings.MessageForceReScanDescription }}
</p>
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">**<strong>Match Books</strong> will attempt to match books in library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.</p>
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
**<strong>{{ $strings.ButtonMatchBooks }}</strong> {{ $strings.MessageMatchBooksDescription }}
</p>
</div>
</template>

View File

@ -7,10 +7,10 @@
</svg>
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
<div class="flex-grow" />
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">Scan</ui-btn>
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">Force Re-Scan</ui-btn>
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">{{ $strings.ButtonScan }}</ui-btn>
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">{{ $strings.ButtonForceReScan }}</ui-btn>
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">Match Books</ui-btn>
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">{{ $strings.ButtonMatchBooks }}</ui-btn>
<span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
<span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>

View File

@ -3,20 +3,20 @@
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex flex-wrap -mx-1">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
</div>
<div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" />
</div>
</div>
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-3/4 px-1">
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" endpoint="authors/search" />
</div>
<div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" />
</div>
</div>
@ -26,20 +26,20 @@
</div>
</div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
</div>
<div class="flex-grow px-1 mt-2 md:mt-0">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
</div>
</div>
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" />
</div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
@ -51,14 +51,14 @@
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
</div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
</div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<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>

View File

@ -2,22 +2,22 @@
<div class="w-full py-2">
<div class="flex -mb-px">
<div class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
<p class="text-sm">Scheduler</p>
<p class="text-sm">{{ $strings.HeaderSchedule }}</p>
</div>
<div class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
<p class="text-sm">Advanced</p>
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
</div>
</div>
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 280px">
<template v-if="!showAdvancedView">
<ui-dropdown v-model="selectedInterval" @input="updateCron" label="Interval" :items="intervalOptions" class="mb-2" />
<ui-dropdown v-model="selectedInterval" @input="updateCron" :label="$strings.LabelInterval" :items="intervalOptions" class="mb-2" />
<ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" label="Weekdays to run" :items="weekdays" />
<ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" :label="$string.LabelWeekdaysToRun" :items="weekdays" />
<div v-if="(selectedWeekdays.length && selectedInterval === 'custom') || selectedInterval === 'daily'" class="flex items-center py-2">
<ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" label="Hour" class="max-w-20" />
<ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" :label="$strings.LabelHour" class="max-w-20" />
<p class="text-xl px-2 mt-4">:</p>
<ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" label="Minute" class="max-w-20" />
<ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" :label="$strings.LabelMinute" class="max-w-20" />
</div>
<div v-if="description" class="w-full bg-primary bg-opacity-75 rounded-xl p-2 md:p-4 text-center mt-2">
@ -25,15 +25,15 @@
</div>
</template>
<template v-else>
<p class="px-1 text-sm font-semibold">Cron Expression</p>
<ui-text-input ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" label="Cron Expression" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" />
<p class="px-1 text-sm font-semibold">{{ $strings.LabelCronExpression }}</p>
<ui-text-input ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" />
<div class="flex items-center justify-center">
<widgets-loading-spinner v-if="isValidating" class="mr-2" />
<span v-else class="material-icons-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">Checking cron...</p>
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p>
<p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p>
<p v-else class="text-success text-base md:text-lg text-center">Valid cron expression</p>
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
</div>
</template>
</div>
@ -137,31 +137,31 @@ export default {
weekdays() {
return [
{
text: 'Sunday',
text: this.$strings.WeekdaySunday,
value: 0
},
{
text: 'Monday',
text: this.$strings.WeekdayMonday,
value: 1
},
{
text: 'Tuesday',
text: this.$strings.WeekdayTuesday,
value: 2
},
{
text: 'Wednesday',
text: this.$strings.WeekdayWednesday,
value: 3
},
{
text: 'Thursday',
text: this.$strings.WeekdayThursday,
value: 4
},
{
text: 'Friday',
text: this.$strings.WeekdayFriday,
value: 5
},
{
text: 'Saturday',
text: this.$strings.WeekdaySaturday,
value: 6
}
]

View File

@ -1,6 +1,6 @@
<template>
<div>
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" label="Series" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" />
</div>

View File

@ -8,23 +8,23 @@
<span class="material-icons text-base">edit</span>
</button>
<div class="flex-grow" />
<p class="text-base">Duration:</p>
<p class="text-base">{{ $strings.LabelDuration }}:</p>
<p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
</div>
<div class="flex flex-wrap-reverse justify-center py-4">
<div class="w-full max-w-3xl py-4">
<div class="flex items-center">
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
<div class="flex-grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
<div class="w-40" />
</div>
<div class="flex items-center mb-3 py-1">
<div class="flex-grow" />
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">Shift Times</ui-btn>
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
<ui-btn color="success" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
<div class="w-40" />
</div>
@ -34,13 +34,13 @@
<div class="w-12"></div>
<div class="flex-grow">
<div class="flex items-center">
<p class="text-sm mb-1 font-semibold pr-2">Time to shift in seconds</p>
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
<ui-text-input v-model="shiftAmount" type="number" class="max-w-20" style="height: 30px" />
<ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">Add</ui-btn>
<ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">{{ $strings.ButtonAdd }}</ui-btn>
<div class="flex-grow" />
<span class="material-icons text-gray-200 hover:text-white cursor-pointer" @click="showShiftTimes = false">close</span>
</div>
<p class="text-xs py-1.5 text-gray-300 max-w-md">Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.</p>
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
</div>
<div class="w-40"></div>
</div>
@ -49,8 +49,8 @@
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="w-12"></div>
<div class="w-32 px-2">Start</div>
<div class="flex-grow px-2">Title</div>
<div class="w-32 px-2">{{ $strings.LabelStart }}</div>
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
<div class="w-40"></div>
</div>
<template v-for="chapter in newChapters">
@ -69,7 +69,7 @@
<span class="material-icons-outlined text-base">remove</span>
</button>
<ui-tooltip text="Insert chapter below" direction="bottom">
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
<span class="material-icons text-lg">add</span>
</button>
@ -93,11 +93,11 @@
</div>
<div class="w-full max-w-xl py-4">
<p class="text-lg mb-4 font-semibold py-1">Audio Tracks</p>
<p class="text-lg mb-4 font-semibold py-1">{{ $strings.HeaderAudioTracks }}</p>
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="flex-grow">Filename</div>
<div class="w-20">Duration</div>
<div class="w-20 text-center">Chapters</div>
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
<div class="w-20">{{ $strings.LabelDuration }}</div>
<div class="w-20 text-center">{{ $strings.HeaderChapters }}</div>
</div>
<template v-for="track in audioTracks">
<div :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success bg-opacity-10' : ''">
@ -122,14 +122,14 @@
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">Find Chapters</p>
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
</div>
</template>
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
<div v-if="!chapterData" class="flex p-20">
<ui-text-input-with-label v-model="asinInput" label="ASIN" />
<ui-dropdown v-model="regionInput" label="Region" small :items="audibleRegions" class="w-32 mx-1" />
<ui-btn small color="primary" class="mt-5" @click="findChapters">Find</ui-btn>
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
<ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
</div>
<div v-else class="w-full p-4">
<div class="flex justify-between mb-4">
@ -148,8 +148,8 @@
<widgets-alert v-else-if="chapterData.runtimeLengthSec < mediaDurationRounded" type="warning" class="mb-2"> Your audiobook duration is longer than the duration found </widgets-alert>
<div class="flex py-0.5 text-xs font-semibold uppercase text-gray-300 mb-1">
<div class="w-24 px-2">Start</div>
<div class="flex-grow px-2">Title</div>
<div class="w-24 px-2">{{ $strings.LabelStart }}</div>
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
</div>
<div class="w-full max-h-80 overflow-y-auto my-2">
<div v-for="(chapter, index) in chapterData.chapters" :key="index" class="flex py-0.5 text-xs" :class="chapter.startOffsetSec > mediaDuration ? 'bg-error bg-opacity-20' : chapter.startOffsetSec + chapter.lengthMs / 1000 > mediaDuration ? 'bg-warning bg-opacity-20' : index % 2 === 0 ? 'bg-primary bg-opacity-30' : ''">
@ -172,12 +172,12 @@
</div>
</div>
<div class="flex items-center pt-2">
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">Map Chapter Titles</ui-btn>
<ui-tooltip text="Map chapter titles to your existing audiobook chapters without adjusting timestamps" direction="top">
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top">
<span class="material-icons-outlined">info</span>
</ui-tooltip>
<div class="flex-grow" />
<ui-btn small color="success" @click="applyChapterData">Apply Chapters</ui-btn>
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
<div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
<div class="flex items-center justify-center mb-6">
<div class="w-full max-w-2xl">
<p class="text-2xl mb-2">Audiobook File Management Tools</p>
<p class="text-2xl mb-2">{{ $strings.HeaderAudiobookTools }}</p>
</div>
<div class="w-full max-w-2xl">
<div class="flex justify-end">

View File

@ -2,15 +2,16 @@
<div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2">
<h1 class="text-xl">Backups</h1>
<h1 class="text-xl">{{ $strings.HeaderBackups }}</h1>
</div>
<p class="text-base mb-4 text-gray-300">Backups include users, user progress, library item details, server settings, and images stored in <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>. <br />Backups <strong>do not</strong> include any files stored in your library folders.</p>
<p class="text-base mb-2 text-gray-300">{{ $strings.MessageBackupsDescription }} <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>.</p>
<p class="text-base mb-4 text-gray-300">{{ $strings.MessageBackupsNote }}</p>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
<ui-tooltip :text="backupsTooltip">
<p class="pl-4 text-lg">Enable automatic backups <span class="material-icons icon-text">info_outlined</span></p>
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
<p class="pl-4 text-lg">{{ $strings.LabelBackupsEnableAutomaticBackups }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
@ -25,16 +26,16 @@
<div class="flex items-center py-2">
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
<ui-tooltip :text="numBackupsToKeepTooltip">
<p class="pl-4 text-lg">Number of backups to keep <span class="material-icons icon-text">info_outlined</span></p>
<ui-tooltip :text="$strings.LabelBackupsNumberToKeepHelp">
<p class="pl-4 text-lg">{{ $strings.LabelBackupsNumberToKeep }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-text-input type="number" v-model="maxBackupSize" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
<ui-tooltip :text="maxBackupSizeTooltip">
<p class="pl-4 text-lg">Maximum backup size (in GB) <span class="material-icons icon-text">info_outlined</span></p>
<ui-tooltip :text="$strings.LabelBackupsMaxBackupSizeHelp">
<p class="pl-4 text-lg">{{ $strings.LabelBackupsMaxBackupSize }} <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
@ -55,10 +56,7 @@ export default {
maxBackupSize: 1,
cronExpression: '',
newServerSettings: {},
showCronBuilder: false,
backupsTooltip: 'Backups saved in /metadata/backups',
numBackupsToKeepTooltip: 'Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.',
maxBackupSizeTooltip: 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
showCronBuilder: false
}
},
watch: {

View File

@ -109,9 +109,9 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
<p class="pl-4">
Use Overdrive Media Markers for chapters
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -119,9 +119,9 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferAudioMetadata">
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
<p class="pl-4">
Prefer audio metadata
{{ $strings.LabelSettingsPreferAudioMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -129,9 +129,9 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
<p class="pl-4">
Prefer OPF metadata
{{ $strings.LabelSettingsPreferOPFMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -139,9 +139,9 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata">
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4">
Prefer matched metadata
{{ $strings.LabelSettingsPreferMatchedMetadata }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -149,33 +149,23 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
<ui-tooltip :text="tooltips.scannerDisableWatcher">
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
<p class="pl-4">
Disable Watcher
{{ $strings.LabelSettingsDisableWatcher }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<!-- <div class="flex items-center py-2">
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
<ui-tooltip :text="tooltips.scannerMaxThreads">
<p class="pl-4">
Max # of threads to use
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="showExperimentalFeatures" />
<ui-tooltip :text="tooltips.experimentalFeatures">
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
<p class="pl-4">
Experimental features
{{ $strings.LabelSettingsExperimentalFeatures }}
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-icons icon-text text-sm">info_outlined</span>
</a>
@ -185,9 +175,9 @@
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
<ui-tooltip :text="tooltips.enableEReader">
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
<p class="pl-4">
Enable e-reader for all users
{{ $strings.LabelSettingsEnableEReader }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
@ -228,7 +218,7 @@
</svg>
</a>
<p class="pl-4 pr-2 text-sm font-book text-yellow-400">
Join us on
{{ $strings.MessageJoinUsOn }}
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
</p>
<a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
@ -276,17 +266,6 @@ export default {
useBookshelfView: false,
isPurgingCache: false,
newServerSettings: {},
tooltips: {
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
scannerPreferMatchedMetadata: 'Matched data will overide book details when using Quick Match',
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
},
showConfirmPurgeCache: false
}
},
@ -317,26 +296,6 @@ export default {
}
},
methods: {
updateScannerMaxThreads(val) {
if (!val || isNaN(val)) {
this.$toast.error('Invalid max threads must be a number')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Number(val) < 0) {
this.$toast.error('Max threads must be >= 0')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Math.round(Number(val)) !== Number(val)) {
this.$toast.error('Max threads must be an integer')
this.newServerSettings.scannerMaxThreads = 0
return
}
this.updateServerSettings({
scannerMaxThreads: Number(val)
})
},
updateSortingPrefixes(val) {
if (!val || !val.length) {
this.$toast.error('Must have at least 1 prefix')
@ -390,7 +349,7 @@ export default {
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
},
resetLibraryItems() {
if (confirm('WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
if (confirm(this.$strings.MessageRemoveAllItemsWarning)) {
this.isResettingLibraryItems = true
this.$axios
.$delete('/api/items/all')

View File

@ -1,12 +1,12 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
<h1 class="text-xl">{{ $strings.HeaderLibraryStats }}: {{ currentLibraryName }}</h1>
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
<p v-if="!top5Genres.length">No Genres</p>
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
<template v-for="genre in top5Genres">
<div :key="genre.genre" class="w-full py-2">
<div class="flex items-end mb-1">
@ -23,8 +23,8 @@
</template>
</div>
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 10 Authors</h1>
<p v-if="!top10Authors.length">No Authors</p>
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
<template v-for="(author, index) in top10Authors">
<div :key="author.id" class="w-full py-2">
<div class="flex items-center mb-1">
@ -42,8 +42,8 @@
</template>
</div>
<div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Longest Items (hrs)</h1>
<p v-if="!top10LongestItems.length">No Items</p>
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
<template v-for="(ab, index) in top10LongestItems">
<div :key="index" class="w-full py-2">
<div class="flex items-center mb-1">

View File

@ -2,7 +2,7 @@
<div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2">
<h1 class="text-xl">Logs</h1>
<h1 class="text-xl">{{ $strings.HeaderLogs }}</h1>
</div>
<div class="flex justify-between mb-2 place-items-end">
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
@ -22,7 +22,7 @@
</div>
<div v-if="!logs.length" class="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-center">
<p class="text-xl text-gray-200 mb-2">No Logs</p>
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto">
<h2 class="text-xl font-semibold mb-4">Apprise Notification Settings</h2>
<h2 class="text-xl font-semibold mb-4">{{ $strings.HeaderAppriseNotificationSettings }}</h2>
<p class="mb-6 text-gray-200">
In order to use this feature you will need to have an instance of <a href="https://github.com/caronc/apprise-api" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
<span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337</span> then you would put <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337/notify</span>.
@ -13,33 +13,33 @@
<div class="flex items-center py-2">
<ui-text-input ref="maxNotificationQueueInput" type="number" v-model="maxNotificationQueue" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" />
<ui-tooltip text="Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming." direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">Max queue size for notification events<span class="material-icons icon-text ml-1">info_outlined</span></p>
<ui-tooltip :text="$strings.LabelNotificationsMaxQueueSizeHelp" direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">{{ $strings.LabelNotificationsMaxQueueSize }}<span class="material-icons icon-text ml-1">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-text-input ref="maxFailedAttemptsInput" type="number" v-model="maxFailedAttempts" no-spinner :disabled="savingSettings" :padding-x="1" text-center class="w-10" />
<ui-tooltip text="Notifications are disabled once they fail to send this many times." direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">Max failed attempts<span class="material-icons icon-text ml-1">info_outlined</span></p>
<ui-tooltip :text="$strings.LabelNotificationsMaxFailedAttemptsHelp" direction="right">
<p class="pl-2 md:pl-4 text-base md:text-lg">{{ $strings.LabelNotificationsMaxFailedAttempts }}<span class="material-icons icon-text ml-1">info_outlined</span></p>
</ui-tooltip>
</div>
<div class="flex items-center justify-end pt-4">
<ui-btn :loading="savingSettings" type="submit">Save</ui-btn>
<ui-btn :loading="savingSettings" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</form>
<div class="w-full h-px bg-white bg-opacity-10 my-6" />
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold">Notifications</h2>
<ui-btn small color="success" class="flex items-center" @click="clickCreate">Create <span class="material-icons text-lg pl-2">add</span></ui-btn>
<h2 class="text-xl font-semibold">{{ $strings.HeaderNotifications }}</h2>
<ui-btn small color="success" class="flex items-center" @click="clickCreate">{{ $strings.ButtonCreate }} <span class="material-icons text-lg pl-2">add</span></ui-btn>
</div>
<div v-if="!notifications.length" class="flex justify-center text-center">
<p class="text-lg text-gray-200">No notifications</p>
<p class="text-lg text-gray-200">{{ $strings.MessageNoNotifications }}</p>
</div>
<template v-for="notification in notifications">
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />

View File

@ -2,23 +2,23 @@
<div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2">
<h1 class="text-xl">Listening Sessions</h1>
<h1 class="text-xl">{{ $strings.HeaderListeningSessions }}</h1>
</div>
<div class="flex justify-end mb-2">
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
</div>
<div v-if="listeningSessions.length" class="block max-w-full">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
<th class="w-32 min-w-32">Listened</th>
<th class="w-16 min-w-16">Last Time</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th>
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
@ -55,7 +55,7 @@
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div>
</div>
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
</div>
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
@ -134,7 +134,7 @@ export default {
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {

View File

@ -1,6 +1,6 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<h1 class="text-xl">Stats for {{ username }}</h1>
<h1 class="text-xl">{{ $strings.HeaderYourStats }}</h1>
<div class="flex justify-center">
<div class="flex p-2">
@ -12,7 +12,7 @@
</svg>
<div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
</div>
</div>
@ -22,7 +22,7 @@
</div>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
</div>
</div>
@ -32,7 +32,7 @@
</div>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
</div>
</div>
</div>
@ -40,11 +40,11 @@
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
<div class="w-80 my-6 mx-auto">
<div class="flex mb-4 items-center">
<h1 class="text-2xl font-book">Recent Sessions</h1>
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
<div class="flex-grow" />
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">View All</ui-btn>
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
</div>
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
<p v-if="!mostRecentListeningSessions.length">{{ $strings.MessageNoListeningSessions }}</p>
<template v-for="(item, index) in mostRecentListeningSessions">
<div :key="item.id" class="w-full py-0.5">
<div class="flex items-center mb-1">

View File

@ -6,7 +6,7 @@
<div class="h-10 w-10 flex items-center justify-center">
<span class="material-icons text-2xl">arrow_back</span>
</div>
<p class="pl-1">All Users</p>
<p class="pl-1">{{ $strings.LabelAllUsers }}</p>
</div>
</nuxt-link>
<div class="flex items-center mb-2 mt-4 px-2 sm:px-0">
@ -22,22 +22,22 @@
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats</h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderListeningStats }}</h1>
<div class="flex items-center">
<p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p>
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">View All</ui-btn>
<p class="text-sm text-gray-300">{{ listeningSessions.length }} {{ $strings.HeaderListeningSessions }}</p>
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
</div>
<p class="text-sm text-gray-300">
Total Time Listened:&nbsp;
{{ $strings.LabelTotalTimeListened }}:&nbsp;
<span class="font-mono text-base">{{ listeningTimePretty }}</span>
</p>
<p v-if="timeListenedToday" class="text-sm text-gray-300">
Time Listened Today:&nbsp;
{{ $strings.LabelTimeListenedToday }}:&nbsp;
<span class="font-mono text-base">{{ $elapsedPrettyExtended(timeListenedToday) }}</span>
</p>
<div v-if="latestSession" class="mt-4">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderLastListeningSession }}</h1>
<p class="text-sm text-gray-300">
<strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span>
</p>
@ -45,21 +45,21 @@
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Saved Media Progress</h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
<div v-if="mediaProgressWithoutMedia.length" class="flex items-center py-2 mb-2">
<p class="text-error">User has media progress for {{ mediaProgressWithoutMedia.length }} items that no longer exist.</p>
<div class="flex-grow" />
<ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">Purge Media Progress</ui-btn>
<ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">{{ $strings.ButtonPurgeMediaProgress }}</ui-btn>
</div>
<table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-16 text-left">Item</th>
<th class="w-16 text-left">{{ $strings.LabelItem }}</th>
<th class="text-left"></th>
<th class="w-32">Progress</th>
<th class="w-40 hidden sm:table-cell">Started At</th>
<th class="w-40 hidden sm:table-cell">Last Update</th>
<th class="w-32">{{ $strings.LabelProgress }}</th>
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr>
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
<td>
@ -90,7 +90,7 @@
</td>
</tr>
</table>
<p v-else class="text-white text-opacity-50">Nothing listened to yet...</p>
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoMediaProgress }}</p>
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@
<div class="h-10 w-10 flex items-center justify-center">
<span class="material-icons text-2xl">arrow_back</span>
</div>
<p class="pl-1">Back to User</p>
<p class="pl-1">{{ $strings.LabelBackToUser }}</p>
</div>
</nuxt-link>
<div class="flex items-center mb-2 mt-4 px-2 sm:px-0">
@ -17,16 +17,16 @@
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
<div v-if="listeningSessions.length">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
<th class="w-32 min-w-32">Listened</th>
<th class="w-16 min-w-16">Last Time</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th>
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
@ -121,7 +121,7 @@ export default {
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {

View File

@ -4,13 +4,13 @@
<!-- Library & folder picker -->
<div class="flex my-6 -mx-2">
<div class="w-1/5 px-2">
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" :disabled="!!items.length" @input="libraryChanged" />
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
</div>
<div class="w-3/5 px-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" label="Folder" />
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
</div>
<div class="w-1/5 px-2">
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly label="Media Type" />
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
</div>
</div>
@ -20,18 +20,22 @@
<!-- Picker display -->
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
<p class="text-2xl text-center">{{ isDragging ? 'Drop files' : "Drag n' drop files or folders" }}</p>
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
<p class="text-center text-sm my-5">or</p>
<div class="w-full max-w-xl mx-auto">
<div class="flex">
<ui-btn class="w-full mx-1" @click="openFilePicker">Choose files</ui-btn>
<ui-btn class="w-full mx-1" @click="openFolderPicker">Choose a folder</ui-btn>
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
<ui-btn class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }}</ui-btn>
</div>
</div>
<div class="pt-8 text-center">
<p class="text-xs text-white text-opacity-50 font-mono mb-4"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
<p class="text-xs text-white text-opacity-50 font-mono mb-4">
<strong>{{ $strings.LabelSupportedFileTypes }}: </strong>{{ inputAccept.join(', ') }}
</p>
<p class="text-sm text-white text-opacity-70">Folders with media files will be treated as separate library items. <span v-if="selectedLibraryMediaType === 'book'">If uploading only audio files then each audio file will be treated as a separate audiobook.</span></p>
<p class="text-sm text-white text-opacity-70">
{{ $strings.NoteUploaderFoldersWithMediaFiles }} <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
</p>
</div>
</div>
<!-- Item list header -->
@ -39,18 +43,20 @@
<p class="text-lg">{{ items.length }} item{{ items.length === 1 ? '' : 's' }}</p>
<p v-if="ignoredFiles.length" class="text-lg">&nbsp;|&nbsp;{{ ignoredFiles.length }} file{{ ignoredFiles.length === 1 ? '' : 's' }} ignored</p>
<div class="flex-grow" />
<ui-btn :disabled="processing" small @click="reset">Reset</ui-btn>
<ui-btn :disabled="processing" small @click="reset">{{ $strings.ButtonReset }}</ui-btn>
</div>
<!-- Alerts -->
<widgets-alert v-if="!items.length && !uploadReady" type="error" class="my-4">
<p class="text-lg">No items found</p>
<p class="text-lg">{{ $strings.MessageNoItemsFound }}</p>
</widgets-alert>
<widgets-alert v-if="ignoredFiles.length" type="warning" class="my-4">
<div class="w-full pr-12">
<p class="text-base mb-1">Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.</p>
<tables-uploaded-files-table :files="ignoredFiles" title="Ignored Files" class="text-white" />
<p class="text-xs text-white text-opacity-50 font-mono pt-1"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
<p class="text-base mb-1">{{ $strings.NoteUploaderUnsupportedFiles }}</p>
<tables-uploaded-files-table :files="ignoredFiles" :title="$strings.HeaderIgnoredFiles" class="text-white" />
<p class="text-xs text-white text-opacity-50 font-mono pt-1">
<strong>{{ $strings.LabelSupportedFileTypes }}: </strong>{{ inputAccept.join(', ') }}
</p>
</div>
</widgets-alert>
@ -61,8 +67,8 @@
<!-- Upload/Reset btns -->
<div v-show="items.length" class="flex justify-end pb-8 pt-4">
<ui-btn v-if="!uploadFinished" color="success" :loading="processing" @click="submit">Upload</ui-btn>
<ui-btn v-else @click="reset">Reset</ui-btn>
<ui-btn v-if="!uploadFinished" color="success" :loading="processing" @click="submit">{{ $strings.ButtonUpload }}</ui-btn>
<ui-btn v-else @click="reset">{{ $strings.ButtonReset }}</ui-btn>
</div>
</div>

View File

@ -2,8 +2,25 @@ import Vue from "vue"
const defaultCode = 'en-us'
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
}
)
}
Vue.prototype.$i18nCode = ''
Vue.prototype.$strings = {}
Vue.prototype.$getString = (key, subs) => {
if (!Vue.prototype.$strings[key]) return ''
if (subs && Array.isArray(subs) && subs.length) {
return supplant(Vue.prototype.$strings[key], subs)
}
return Vue.prototype.$strings[key]
}
var translations = {}

View File

@ -1,54 +1,314 @@
{
"ButtonHome": "Home",
"ButtonLatest": "Latest",
"ButtonLibrary": "Library",
"ButtonSeries": "Series",
"ButtonCollections": "Collections",
"ButtonAdd": "Add",
"ButtonAddChapters": "Add Chapters",
"ButtonAddYourFirstLibrary": "Add your first library",
"ButtonApplyChapters": "Apply Chapters",
"ButtonAuthors": "Authors",
"ButtonSearch": "Search",
"ButtonIssues": "Issues",
"ButtonBrowseForFolder": "Browse for Folder",
"ButtonCancel": "Cancel",
"ButtonChangePasswordSubmit": "Submit",
"ButtonChangeRootPassword": "Change Root Password",
"ButtonChooseAFolder": "Choose a folder",
"ButtonChooseFiles": "Choose files",
"ButtonCollections": "Collections",
"ButtonCreate": "Create",
"ButtonCreateBackup": "Create Backup",
"ButtonEditChapters": "Edit Chapters",
"ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path",
"ButtonHide": "Hide",
"ButtonHome": "Home",
"ButtonIssues": "Issues",
"ButtonLatest": "Latest",
"ButtonLogout": "Logout",
"ButtonLookup": "Lookup",
"ButtonLibrary": "Library",
"ButtonMatchAllAuthors": "Match All Authors",
"ButtonMatchBooks": "Match Books",
"ButtonMapChapterTitles": "Map Chapter Titles",
"ButtonNevermind": "Nevermind",
"ButtonOk": "Ok",
"ButtonOpenManager": "Open Manager",
"ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress",
"ButtonQuickMatch": "Quick Match",
"ButtonRemove": "Remove",
"ButtonRemoveAll": "Remove All",
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
"ButtonReScan": "Re-Scan",
"ButtonReset": "Reset",
"ButtonRestore": "Restore",
"ButtonSave": "Save",
"ButtonSaveAndClose": "Save & Close",
"ButtonScan": "Scan",
"ButtonSearch": "Search",
"ButtonSelectFolderPath": "Select Folder Path",
"ButtonSeries": "Series",
"ButtonShiftTimes": "Shift Times",
"ButtonShow": "Show",
"ButtonSubmit": "Submit",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover",
"ButtonViewAll": "View All",
"ButtonYes": "Yes",
"HeaderAccount": "Account",
"HeaderChangePassword": "Change Password",
"HeaderSettings": "Settings",
"HeaderLibraries": "Libraries",
"HeaderUsers": "Users",
"HeaderListeningSessions": "Listening Sessions",
"HeaderAdvanced": "Advanced",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAudioTracks": "Audio Tracks",
"HeaderBackups": "Backups",
"HeaderLogs": "Logs",
"HeaderNotifications": "Notifications",
"HeaderChangePassword": "Change Password",
"HeaderChapters": "Chapters",
"HeaderChooseAFolder": "Choose a Folder",
"HeaderCollection": "Collection",
"HeaderCover": "Cover",
"HeaderDetails": "Details",
"HeaderEpisodes": "Episodes",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files",
"HeaderItemFiles": "Item Files",
"HeaderLastListeningSession": "Last Listening Session",
"HeaderLibraries": "Libraries",
"HeaderLibraryFiles": "Library Files",
"HeaderLibraryStats": "Library Stats",
"HeaderYourStats": "Your Stats",
"HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner",
"HeaderListeningSessions": "Listening Sessions",
"HeaderListeningStats": "Listening Stats",
"HeaderLogs": "Logs",
"HeaderMatch": "Match",
"HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library",
"HeaderNotifications": "Notifications",
"HeaderOtherFiles": "Other Files",
"HeaderPermissions": "Permissions",
"HeaderPreviewCover": "Preview Cover",
"HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSettings": "Settings",
"HeaderSettingsDisplay": "Display",
"HeaderSettingsExperimental": "Experimental Features",
"LabelUsername": "Username",
"HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner",
"HeaderStatsLongestItems": "Longest Items (hrs)",
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
"HeaderStatsRecentSessions": "Recent Sessions",
"HeaderStatsTop10Authors": "Top 10 Authors",
"HeaderStatsTop5Genres": "Top 5 Genres",
"HeaderTools": "Tools",
"HeaderUpdateAccount": "Update Account",
"HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users",
"HeaderYourStats": "Your Stats",
"LabelAccountType": "Account Type",
"LabelPassword": "Password",
"LabelNewPassword": "New Password",
"LabelActivity": "Activity",
"LabelAllUsers": "All Users",
"LabelAuthor": "Author",
"LabelAuthors": "Authors",
"LabelBackToUser": "Back to User",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
"LabelBackupsNumberToKeep": "Number of backups to keep",
"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",
"LabelChangePassword": "Change Password",
"LabelCollapseSeries": "Collapse Series",
"LabelCollections": "Collections",
"LabelConfirmPassword": "Confirm Password",
"LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL",
"LabelCreatedAt": "Created At",
"LabelCronExpression": "Cron Expression",
"LabelCurrently": "Currently:",
"LabelDatetime": "Datetime",
"LabelDescription": "Description",
"LabelDeviceInfo": "Device Info",
"LabelDirectory": "Directory",
"LabelDownload": "Download",
"LabelDuration": "Duration",
"LabelEnable": "Enable",
"LabelEnd": "End",
"LabelExplicit": "Explicit",
"LabelFile": "File",
"LabelFilename": "Filename",
"LabelFilterByUser": "Filter by User",
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
"LabelGenres": "Genres",
"LabelHour": "Hour",
"LabelIcon": "Icon",
"LabelInterval": "Interval",
"LabelItem": "Item",
"LabelLanguage": "Language",
"LabelLastSeen": "Last Seen",
"LabelLastTime": "Last Time",
"LabelLastUpdate": "Last Update",
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
"LabelLibrary": "Library",
"LabelLibraryName": "Library Name",
"LabelMarkSeries": "Mark Series",
"LabelMediaType": "Media Type",
"LabelMetadataProvider": "Metadata Provider",
"LabelMinute": "Minute",
"LabelName": "Name",
"LabelNarrators": "Narrators",
"LabelNewPassword": "New Password",
"LabelNotFinished": "Not Finished",
"LabelNotificationEvent": "Notification Event",
"LabelNotificationAppriseURL": "Apprise URL(s)",
"LabelNotificationAvailableVariables": "Available variables",
"LabelNotificationBodyTemplate": "Body Template",
"LabelNotificationTitleTemplate": "Title Template",
"LabelNotificationsMaxFailedAttempts": "Max failed attempts",
"LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
"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.",
"LabelPassword": "Password",
"LabelPath": "Path",
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
"LabelPermissionsAccessAllTags": "Can Access All Tags",
"LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
"LabelPermissionsDelete": "Can Delete",
"LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload",
"LabelPlayMethod": "Play Method",
"LabelPodcasts": "Podcasts",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelProgress": "Progress",
"LabelProvider": "Provider",
"LabelPublisher": "Publisher",
"LabelPublishYear": "Publish Year",
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSequence": "Sequence",
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
"LabelSettingsChromecastSupport": "Chromecast support",
"LabelSettingsDateFormat": "Date Format",
"LabelSettingsDisableWatcher": "Disable Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEnableEReader": "Enable e-reader for all users",
"LabelSettingsEnableEReaderHelp": "E-reader is still a work in progress, but use this setting to open it up to all your users (or use the \"Experimental Features\" toggle just for use by you)",
"LabelSettingsExperimentalFeatures": "Experimental features",
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
"LabelSettingsFindCovers": "Find covers",
"LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferAudioMetadata": "Prefer audio metadata",
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
"LabelSettingsStoreCoversWithItem": "Store covers with item",
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
"LabelSettingsStoreMetadataWithItem": "Store metadata with item",
"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",
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelSettingsChromecastSupport": "Chromecast support",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
"LabelSettingsDateFormat": "Date Format",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsFindCovers": "Find covers",
"LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time",
"LabelSettingsSquareBookCovers": "User square book covers",
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
"LabelSize": "Size",
"LabelStart": "Start",
"LabelStartedAt": "Started At",
"LabelStatsAudioTracks": "Audio Tracks",
"LabelStatsAuthors": "Authors",
"LabelStatsBestDay": "Best Day",
"LabelStatsDailyAverage": "Daily Average",
"LabelStatsDays": "Days",
"LabelStatsDaysListened": "Days Listened",
"LabelStatsHours": "Hours",
"LabelStatsInARow": "in a row",
"LabelStatsItemsFinished": "Items Finished",
"LabelStatsItemsInLibrary": "Items in Library",
"LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes Listening",
"LabelStatsOverall": "Overall",
"LabelStatsWeekListening": "Week Listening",
"LabelSubtitle": "Subtitle",
"LabelSupportedFileTypes": "Supported File Types",
"LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTimeListened": "Time Listened",
"LabelTimeListenedToday": "Time Listened Today",
"LabelTimeToShift": "Time to shift in seconds",
"LabelTitle": "Title",
"LabelTotalTimeListened": "Total Time Listened",
"LabelType": "Type",
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files",
"LabelUser": "User",
"LabelUsername": "Username",
"LabelWeekdaysToRun": "Weekdays to run",
"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.",
"MessageCheckingCron": "Checking cron...",
"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!",
"MessageInsertChapterBelow": "Insert chapter below",
"MessageJoinUsOn": "Join us on",
"MessageLoading": "Loading...",
"MessageLoadingFolders": "Loading folders...",
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
"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.",
"MessageNoAuthors": "No Authors",
"MessageNoChapters": "No Chapters",
"MessageNoCoversFound": "No Covers Found",
"MessageNoFoldersAvailable": "No Folders Available",
"MessageNoGenres": "No Genres",
"MessageNoItems": "No Items",
"MessageNoItemsFound": "No items found",
"MessageNoListeningSessions": "No Listening Sessions",
"MessageNoLogs": "No Logs",
"MessageNoMediaProgress": "No Media Progress",
"MessageNoNotifications": "No Notifications",
"MessageNoResults": "No Results",
"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?",
"MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute 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.",
"MessageSearchResultsFor": "Search results for",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
"MessageUploaderItemFailed": "Failed to upload",
"MessageUploaderItemSuccess": "Successfully Uploaded!",
"MessageUploading": "Uploading...",
"MessageValidCronExpression": "Valid cron expression",
"MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
"NoteChangeRootPassword": "Root user is the only user that can have an empty password",
"SearchPlaceholder": "Search.."
"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",
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
"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.",
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
"PlaceholderNewFolderPath": "New folder path",
"PlaceholderSearch": "Search..",
"WeekdayFriday": "Friday",
"WeekdayMonday": "Monday",
"WeekdaySaturday": "Saturday",
"WeekdaySunday": "Sunday",
"WeekdayThursday": "Thursday",
"WeekdayTuesday": "Tuesday",
"WeekdayWednesday": "Wednesday"
}