diff --git a/.gitignore b/.gitignore index 6f47029b..9360600a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,12 @@ /podcasts/ /media/ /metadata/ -test/ /client/.nuxt/ /client/dist/ /dist/ /deploy/ +/coverage/ +/.nyc_output/ sw.* .DS_STORE diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fb8b48f..397b9618 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ }, "editor.formatOnSave": true, "editor.detectIndentation": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "javascript.format.semicolons": "remove" } \ No newline at end of file diff --git a/client/assets/app.css b/client/assets/app.css index b7b8499d..1a83dc1c 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -258,4 +258,24 @@ Bookshelf Label .no-bars .Vue-Toastification__container.top-right { padding-top: 8px; +} + +.abs-btn::before { + content: ''; + position: absolute; + border-radius: 6px; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0); + transition: all 0.1s ease-in-out; +} + +.abs-btn:hover:not(:disabled)::before { + background-color: rgba(255, 255, 255, 0.1); +} + +.abs-btn:disabled::before { + background-color: rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 92599c7a..c281f821 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -320,9 +320,11 @@ export default { checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', - checkboxDefaultValue: true, + checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0), callback: (confirmed, hardDelete) => { if (confirmed) { + localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1) + this.$store.commit('setProcessingBatch', true) this.$axios diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index fbed11be..15f34867 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -338,9 +338,15 @@ export default { libraryItemsAdded(libraryItems) { console.log('libraryItems added', libraryItems) - const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId) - if (!this.search && isThisLibrary) { - this.fetchCategories() + const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added') + if (!recentlyAddedShelf) return + + // Add new library item to the recently added shelf + for (const libraryItem of libraryItems) { + if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) { + // Add to front of array + recentlyAddedShelf.entities.unshift(libraryItem) + } } }, libraryItemsUpdated(items) { diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 9b79bc0f..ab33a5b3 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -36,7 +36,7 @@ -

{{ $strings.ButtonSearch }}

+

{{ $strings.ButtonAdd }}

diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 267aabaa..c2db0725 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -104,6 +104,11 @@ export default { id: 'config-rss-feeds', title: this.$strings.HeaderRSSFeeds, path: '/config/rss-feeds' + }, + { + id: 'config-authentication', + title: this.$strings.HeaderAuthentication, + path: '/config/authentication' } ] diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index deb96a6c..56207526 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -82,7 +82,7 @@ -

{{ $strings.ButtonSearch }}

+

{{ $strings.ButtonAdd }}

diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index db4e7e9a..fc3bc4b2 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -8,7 +8,7 @@

{{ name }}

-

{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}

+

{{ numBooks }} {{ $strings.LabelBooks }}

diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 21d97b20..075c0fd4 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -15,24 +15,33 @@
- +
- +
+ + +
+ sync +
+
+

{{ $strings.LabelDirectory }} (auto)

- +
- +
-

{{ $strings.LabelDirectory }} (auto)

- + +
@@ -48,8 +57,8 @@

{{ $strings.MessageUploaderItemFailed }}

-
- +
+
@@ -61,10 +70,11 @@ export default { props: { item: { type: Object, - default: () => {} + default: () => { } }, mediaType: String, - processing: Boolean + processing: Boolean, + provider: String }, data() { return { @@ -76,7 +86,8 @@ export default { error: '', isUploading: false, uploadFailed: false, - uploadSuccess: false + uploadSuccess: false, + isFetchingMetadata: false } }, computed: { @@ -87,12 +98,19 @@ export default { if (!this.itemData.title) return '' if (this.isPodcast) return this.itemData.title - if (this.itemData.series && this.itemData.author) { - return Path.join(this.itemData.author, this.itemData.series, this.itemData.title) - } else if (this.itemData.author) { - return Path.join(this.itemData.author, this.itemData.title) - } else { - return this.itemData.title + const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title] + const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part)) + + return Path.join(...cleanedOutputPathParts) + }, + isNonInteractable() { + return this.isUploading || this.isFetchingMetadata + }, + nonInteractionLabel() { + if (this.isUploading) { + return this.$strings.MessageUploading + } else if (this.isFetchingMetadata) { + return this.$strings.LabelFetchingMetadata } } }, @@ -105,9 +123,42 @@ export default { titleUpdated() { this.error = '' }, + async fetchMetadata() { + if (!this.itemData.title.trim().length) { + return + } + + this.isFetchingMetadata = true + this.error = '' + + try { + const searchQueryString = new URLSearchParams({ + title: this.itemData.title, + author: this.itemData.author, + provider: this.provider + }) + const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`) + + if (bestCandidate) { + this.itemData = { + ...this.itemData, + title: bestCandidate.title, + author: bestCandidate.author, + series: (bestCandidate.series || [])[0]?.series + } + } else { + this.error = this.$strings.ErrorUploadFetchMetadataNoResults + } + } catch (e) { + console.error('Failed', e) + this.error = this.$strings.ErrorUploadFetchMetadataAPI + } finally { + this.isFetchingMetadata = false + } + }, getData() { if (!this.itemData.title) { - this.error = 'Must have a title' + this.error = this.$strings.ErrorUploadLacksTitle return null } this.error = '' @@ -128,4 +179,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 1b87df0f..c4d1345d 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -848,9 +848,11 @@ export default { checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', - checkboxDefaultValue: true, + checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0), callback: (confirmed, hardDelete) => { if (confirmed) { + localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1) + this.processing = true const axios = this.$axios || this.$nuxt.$axios axios diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index 4081f98c..7be286fe 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -14,8 +14,7 @@ export default { }, data() { return { - tracks: [], - showFullPath: false + tracks: [] } }, watch: { diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 5bcdabed..2a68dd63 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -127,7 +127,7 @@ export default { skipMatchingMediaWithIsbn: false, autoScanCronExpression: null, hideSingleBookSeries: false, - metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } }, diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 215f79b5..8ec73dd0 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -19,9 +19,11 @@
  • reorder
    - {{ source.include ? index + 1 : '' }} + {{ source.include ? getSourceIndex(source.id) : '' }} +
    +
    + {{ source.name }} {{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}
    -
    {{ source.name }}
    @@ -64,6 +66,11 @@ export default { name: 'Audio file meta tags', include: true }, + nfoFile: { + id: 'nfoFile', + name: 'NFO file', + include: true + }, txtFiles: { id: 'txtFiles', name: 'desc.txt & reader.txt files', @@ -92,20 +99,34 @@ export default { }, isBookLibrary() { return this.mediaType === 'book' + }, + firstActiveSourceIndex() { + return this.metadataSourceMapped.findIndex((source) => source.include) + }, + lastActiveSourceIndex() { + return this.metadataSourceMapped.findLastIndex((source) => source.include) } }, methods: { + getSourceIndex(source) { + const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse() + return activeSources.findIndex((s) => s === source) + 1 + }, resetToDefault() { this.metadataSourceMapped = [] for (const key in this.metadataSourceData) { this.metadataSourceMapped.push({ ...this.metadataSourceData[key] }) } + this.metadataSourceMapped.reverse() + this.$emit('update', this.getLibraryData()) }, getLibraryData() { + const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataSourceIds.reverse() return { settings: { - metadataPrecedence: this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataPrecedence: metadataSourceIds } } }, @@ -120,15 +141,16 @@ export default { }, init() { const metadataPrecedence = this.librarySettings.metadataPrecedence || [] - this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s) for (const sourceKey in this.metadataSourceData) { if (!metadataPrecedence.includes(sourceKey)) { const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false } - this.metadataSourceMapped.push(unusedSourceData) + this.metadataSourceMapped.unshift(unusedSourceData) } } + + this.metadataSourceMapped.reverse() } }, mounted() { diff --git a/client/components/tables/EbookFilesTable.vue b/client/components/tables/EbookFilesTable.vue index 0c85774c..3532c2a7 100644 --- a/client/components/tables/EbookFilesTable.vue +++ b/client/components/tables/EbookFilesTable.vue @@ -6,7 +6,7 @@ {{ ebookFiles.length }}
  • - +
    expand_more
    @@ -75,6 +75,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, readEbook(fileIno) { this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno }) }, @@ -82,6 +86,10 @@ export default { this.showFiles = !this.showFiles } }, - mounted() {} + mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } + } } \ No newline at end of file diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue index c6c8c777..fef1ae5a 100644 --- a/client/components/tables/LibraryFilesTable.vue +++ b/client/components/tables/LibraryFilesTable.vue @@ -6,7 +6,7 @@ {{ files.length }}
    - +
    expand_more
    @@ -84,6 +84,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, clickBar() { this.showFiles = !this.showFiles }, @@ -93,6 +97,9 @@ export default { } }, mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } this.showFiles = this.expanded } } diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue index 2554fff1..5730ee36 100644 --- a/client/components/tables/TracksTable.vue +++ b/client/components/tables/TracksTable.vue @@ -6,7 +6,7 @@ {{ tracks.length }}
    - + {{ $strings.ButtonManageTracks }} @@ -74,6 +74,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, clickBar() { this.showTracks = !this.showTracks }, @@ -82,6 +86,10 @@ export default { this.showAudioFileDataModal = true } }, - mounted() {} + mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } + } } \ No newline at end of file diff --git a/client/components/ui/Btn.vue b/client/components/ui/Btn.vue index d9b75715..7f73a956 100644 --- a/client/components/ui/Btn.vue +++ b/client/components/ui/Btn.vue @@ -1,5 +1,5 @@