diff --git a/komga-webui/src/components/ReusableDialogs.vue b/komga-webui/src/components/ReusableDialogs.vue index 8b68924a6..0494bc105 100644 --- a/komga-webui/src/components/ReusableDialogs.vue +++ b/komga-webui/src/components/ReusableDialogs.vue @@ -58,6 +58,7 @@ BookDto, Array as () => BookDto[]], required: true, }, + additionalRoles: { + type: Array as () => string[], + required: false, + default: () => [], + }, }, watch: { value(val) { @@ -549,7 +554,7 @@ export default Vue.extend({ remoteRoles = this.books.flatMap(b => b.metadata.authors).map(a => a.role) else if (this.books?.metadata?.authors) remoteRoles = this.books.metadata.authors.map(a => a.role) - const allRoles = this.$_.uniq([...authorRoles, ...remoteRoles, ...this.customRoles]) + const allRoles = this.$_.uniq([...authorRoles, ...remoteRoles, ...this.customRoles, ...this.additionalRoles]) return allRoles.map((role: string) => ({ name: this.$te(`author_roles.${role}`) ? this.$t(`author_roles.${role}`).toString() : role, value: role, diff --git a/komga-webui/src/components/dialogs/EditOneshotDialog.vue b/komga-webui/src/components/dialogs/EditOneshotDialog.vue index c8b364c86..5eae44063 100644 --- a/komga-webui/src/components/dialogs/EditOneshotDialog.vue +++ b/komga-webui/src/components/dialogs/EditOneshotDialog.vue @@ -644,6 +644,7 @@ export default Vue.extend({ genresAvailable: [] as string[], tagsAvailable: [] as string[], sharingLabelsAvailable: [] as string[], + isMultiBookAuthorDirty: false, // workaround for author consistency in bulk mode } }, props: { @@ -652,6 +653,11 @@ export default Vue.extend({ type: [Object as () => Oneshot, Array as () => Oneshot[]], required: true, }, + additionalRoles: { + type: Array as () => string[], + required: false, + default: () => [], + }, }, watch: { value(val) { @@ -729,7 +735,7 @@ export default Vue.extend({ remoteRoles = this.books.flatMap(b => b.metadata.authors).map(a => a.role) else if (this.books?.metadata?.authors) remoteRoles = this.books.metadata.authors.map(a => a.role) - const allRoles = this.$_.uniq([...authorRoles, ...remoteRoles, ...this.customRoles]) + const allRoles = this.$_.uniq([...authorRoles, ...remoteRoles, ...this.customRoles, ...this.additionalRoles]) return allRoles.map((role: string) => ({ name: this.$te(`author_roles.${role}`) ? this.$t(`author_roles.${role}`).toString() : role, value: role, diff --git a/komga-webui/src/functions/author-roles.ts b/komga-webui/src/functions/author-roles.ts new file mode 100644 index 000000000..9767c4de5 --- /dev/null +++ b/komga-webui/src/functions/author-roles.ts @@ -0,0 +1,14 @@ +import {authorRoles} from '@/types/author-roles' +import {BookDto} from '@/types/komga-books' + +export function getCustomRoles(books: BookDto[]): string[] { + return books.flatMap((b) => b.metadata.authors.map((a) => a.role)).filter((ra) => !authorRoles.includes(ra)) +} + +export function getCustomRolesForSeries(books: BookDto[], seriesId: string): string[] { + return getCustomRoles(books.filter((b) => b.seriesId === seriesId)) +} + +export function isAllSelectedSameSeries(books: BookDto[]): boolean { + return books.every((b) => b.seriesId === books[0].seriesId) +} \ No newline at end of file diff --git a/komga-webui/src/store.ts b/komga-webui/src/store.ts index 740cf4582..4b345a8ef 100644 --- a/komga-webui/src/store.ts +++ b/komga-webui/src/store.ts @@ -41,6 +41,7 @@ export default new Vuex.Store({ updateBooksDialog: false, deleteBooks: {} as BookDto | BookDto[], deleteBookDialog: false, + updateBooksAdditionalRoles: [] as string[], // books bulk updateBulkBooks: [] as BookDto[], updateBulkBooksDialog: false, @@ -48,6 +49,7 @@ export default new Vuex.Store({ // oneshots updateOneshots: {} as Oneshot | Oneshot[], updateOneshotsDialog: false, + updateOneshotsAdditionalRoles: [] as string[], // series updateSeries: {} as SeriesDto | SeriesDto[], @@ -145,6 +147,9 @@ export default new Vuex.Store({ setUpdateBulkBooks(state, books) { state.updateBulkBooks = books }, + setUpdateBooksAdditionalRoles(state, roles) { + state.updateBooksAdditionalRoles = roles + }, setUpdateBulkBooksDialog(state, dialog) { state.updateBulkBooksDialog = dialog }, @@ -155,6 +160,9 @@ export default new Vuex.Store({ setUpdateOneshotsDialog(state, dialog) { state.updateOneshotsDialog = dialog }, + setUpdateOneshotsAdditionalRoles(state, roles) { + state.updateOneshotsAdditionalRoles = roles + }, // Series setUpdateSeries(state, series) { state.updateSeries = series @@ -246,8 +254,9 @@ export default new Vuex.Store({ commit('setDeleteLibraryDialog', value) }, // books - dialogUpdateBooks({commit}, books) { + dialogUpdateBooks({commit}, {books, roles}) { commit('setUpdateBooks', books) + commit('setUpdateBooksAdditionalRoles', roles || []) commit('setUpdateBooksDialog', true) }, dialogUpdateBooksDisplay({commit}, value) { @@ -269,8 +278,9 @@ export default new Vuex.Store({ commit('setUpdateBulkBooksDialog', value) }, // oneshots - dialogUpdateOneshots({commit}, oneshots) { + dialogUpdateOneshots({commit}, {oneshots, roles}) { commit('setUpdateOneshots', oneshots) + commit('setUpdateOneshotsAdditionalRoles', roles || []) commit('setUpdateOneshotsDialog', true) }, dialogUpdateOneshotsDisplay({commit}, value) { diff --git a/komga-webui/src/views/BrowseBook.vue b/komga-webui/src/views/BrowseBook.vue index b351eb32c..e8617eae0 100644 --- a/komga-webui/src/views/BrowseBook.vue +++ b/komga-webui/src/views/BrowseBook.vue @@ -442,6 +442,7 @@ import BookActionsMenu from '@/components/menus/BookActionsMenu.vue' import ItemCard from '@/components/ItemCard.vue' import ToolbarSticky from '@/components/bars/ToolbarSticky.vue' +import {getCustomRoles} from '@/functions/author-roles' import {groupAuthorsByRole} from '@/functions/authors' import {getBookFormatFromMedia, getBookReadRouteFromMedia} from '@/functions/book-format' import {getPagesLeft, getReadProgress, getReadProgressPercentage} from '@/functions/book-progress' @@ -689,7 +690,8 @@ export default Vue.extend({ this.$komgaBooks.refreshMetadata(this.book) }, editBook() { - this.$store.dispatch('dialogUpdateBooks', this.book) + const customRole = getCustomRoles(this.siblings) + this.$store.dispatch('dialogUpdateBooks', {books: this.book, roles: customRole}) }, removeFromReadList(readListId: string) { const rl = this.readLists.find(x => x.id == readListId) diff --git a/komga-webui/src/views/BrowseBooks.vue b/komga-webui/src/views/BrowseBooks.vue index df0008dad..6e1b16b21 100644 --- a/komga-webui/src/views/BrowseBooks.vue +++ b/komga-webui/src/views/BrowseBooks.vue @@ -175,6 +175,7 @@ import { } from '@/types/komga-search' import i18n from '@/i18n' import {objIsEqual} from '@/functions/object' +import {getCustomRoles} from '@/functions/author-roles' import { FILTER_ANY, FILTER_NONE, @@ -660,10 +661,11 @@ export default Vue.extend({ this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks.map(b => b.id)) }, editSingleBook(book: BookDto) { - this.$store.dispatch('dialogUpdateBooks', book) + this.$store.dispatch('dialogUpdateBooks', {books: book}) }, editMultipleBooks() { - this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) + const customRoles = getCustomRoles(this.selectedBooks) + this.$store.dispatch('dialogUpdateBooks', {books: this.selectedBooks, roles: customRoles}) }, bulkEditMultipleBooks() { this.$store.dispatch('dialogUpdateBulkBooks', this.$_.sortBy(this.selectedBooks, ['metadata.numberSort'])) diff --git a/komga-webui/src/views/BrowseCollection.vue b/komga-webui/src/views/BrowseCollection.vue index feed9cb6b..18e419e49 100644 --- a/komga-webui/src/views/BrowseCollection.vue +++ b/komga-webui/src/views/BrowseCollection.vue @@ -150,6 +150,7 @@ import MultiSelectBar from '@/components/bars/MultiSelectBar.vue' import {LIBRARIES_ALL} from '@/types/library' import {ReadStatus} from '@/types/enum-books' import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series' +import {getCustomRoles} from '@/functions/author-roles' import {mergeFilterParams, toNameValue} from '@/functions/filter' import FilterDrawer from '@/components/FilterDrawer.vue' import FilterPanels from '@/components/FilterPanels.vue' @@ -500,7 +501,7 @@ export default Vue.extend({ const book = (await this.$komgaBooks.getBooksList({ condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)), } as BookSearch)).content[0] - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) } else this.$store.dispatch('dialogUpdateSeries', series) }, @@ -510,7 +511,8 @@ export default Vue.extend({ condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)), } as BookSearch))) const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) } else this.$store.dispatch('dialogUpdateSeries', this.selectedSeries) }, diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index 33f5d0e71..0725590cf 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -132,7 +132,8 @@ import ItemBrowser from '@/components/ItemBrowser.vue' import LibraryNavigation from '@/components/LibraryNavigation.vue' import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue' import PageSizeSelect from '@/components/PageSizeSelect.vue' -import {parseQuerySort} from '@/functions/query-params' +import {getCustomRoles} from '@/functions/author-roles' +import {parseBooleanFilter, parseQuerySort} from '@/functions/query-params' import {ReadStatus} from '@/types/enum-books' import {SeriesStatus} from '@/types/enum-series' import { @@ -873,7 +874,7 @@ export default Vue.extend({ const book = (await this.$komgaBooks.getBooksList({ condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)), } as BookSearch)).content[0] - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) } else this.$store.dispatch('dialogUpdateSeries', series) }, @@ -883,7 +884,8 @@ export default Vue.extend({ condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)), } as BookSearch))) const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) } else this.$store.dispatch('dialogUpdateSeries', this.selectedSeries) }, diff --git a/komga-webui/src/views/BrowseOneshot.vue b/komga-webui/src/views/BrowseOneshot.vue index ac2b11851..cadc17850 100644 --- a/komga-webui/src/views/BrowseOneshot.vue +++ b/komga-webui/src/views/BrowseOneshot.vue @@ -848,7 +848,8 @@ export default Vue.extend({ this.$komgaBooks.refreshMetadata(this.book) }, editBook() { - this.$store.dispatch('dialogUpdateOneshots', {series: this.series, book: this.book} as Oneshot) + const oneshots = {series: this.series, book: this.book} as Oneshot + this.$store.dispatch('dialogUpdateOneshots', {oneshots}) }, removeFromReadList(readListId: string) { const rl = this.readLists.find(x => x.id == readListId) diff --git a/komga-webui/src/views/BrowseReadList.vue b/komga-webui/src/views/BrowseReadList.vue index 71fa26d94..95e0ed721 100644 --- a/komga-webui/src/views/BrowseReadList.vue +++ b/komga-webui/src/views/BrowseReadList.vue @@ -181,6 +181,7 @@ import FilterList from '@/components/FilterList.vue' import {ReadStatus} from '@/types/enum-books' import {authorRoles} from '@/types/author-roles' import {LibraryDto} from '@/types/komga-libraries' +import {isAllSelectedSameSeries, getCustomRolesForSeries} from '@/functions/author-roles' import {mergeFilterParams, toNameValue} from '@/functions/filter' import {Location} from 'vue-router' import {readListFileUrl} from '@/functions/urls' @@ -485,17 +486,17 @@ export default Vue.extend({ async editSingleBook(book: BookDto) { if (book.oneshot) { const series = (await this.$komgaSeries.getOneSeries(book.seriesId)) - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) } else - this.$store.dispatch('dialogUpdateBooks', book) + this.$store.dispatch('dialogUpdateBooks', {books: book}) }, async editMultipleBooks() { if (this.selectedBooks.every(b => b.oneshot)) { const series = await Promise.all(this.selectedBooks.map(b => this.$komgaSeries.getOneSeries(b.seriesId))) const oneshots = this.selectedBooks.map((b, index) => ({series: series[index], book: b} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) + this.$store.dispatch('dialogUpdateOneshots', {oneshots}) } else - this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) + this.$store.dispatch('dialogUpdateBooks', {books: this.selectedBooks}) }, bulkEditMultipleBooks() { this.$store.dispatch('dialogUpdateBulkBooks', this.selectedBooks) diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index 198c51228..d82b39f5c 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -508,6 +508,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue' import ItemCard from '@/components/ItemCard.vue' import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue' import PageSizeSelect from '@/components/PageSizeSelect.vue' +import {getCustomRoles} from '@/functions/author-roles' import {parseQuerySort} from '@/functions/query-params' import {seriesFileUrl, seriesThumbnailUrl} from '@/functions/urls' import {MediaProfile, ReadStatus} from '@/types/enum-books' @@ -1093,10 +1094,12 @@ export default Vue.extend({ this.$store.dispatch('dialogUpdateSeries', this.series) }, editSingleBook(book: BookDto) { - this.$store.dispatch('dialogUpdateBooks', book) + const customRoles = getCustomRoles(this.books) + this.$store.dispatch('dialogUpdateBooks', {books: book, roles: customRoles}) }, editMultipleBooks() { - this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) + const customRoles = getCustomRoles(this.books) + this.$store.dispatch('dialogUpdateBooks', {books: this.selectedBooks, roles: customRoles}) }, bulkEditMultipleBooks() { this.$store.dispatch('dialogUpdateBulkBooks', this.$_.sortBy(this.selectedBooks, ['metadata.numberSort'])) diff --git a/komga-webui/src/views/DashboardView.vue b/komga-webui/src/views/DashboardView.vue index 03a008f4f..d9794452d 100644 --- a/komga-webui/src/views/DashboardView.vue +++ b/komga-webui/src/views/DashboardView.vue @@ -118,6 +118,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue' import ToolbarSticky from '@/components/bars/ToolbarSticky.vue' import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue' import LibraryNavigation from '@/components/LibraryNavigation.vue' +import {getCustomRoles, getCustomRolesForSeries, isAllSelectedSameSeries} from '@/functions/author-roles' import {ReadStatus} from '@/types/enum-books' import {BookDto} from '@/types/komga-books' import { @@ -495,16 +496,19 @@ export default Vue.extend({ let book = (await this.$komgaBooks.getBooksList({ condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)), } as BookSearch)).content[0] - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) - } else + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) + } else { this.$store.dispatch('dialogUpdateSeries', series) + } }, async singleEditBook(book: BookDto) { if (book.oneshot) { const series = (await this.$komgaSeries.getOneSeries(book.seriesId)) - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) - } else - this.$store.dispatch('dialogUpdateBooks', book) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) + } else { + const customRoles = getCustomRolesForSeries(this.getAllBooksFromLoader(), book.seriesId) + this.$store.dispatch('dialogUpdateBooks', {books: book, roles: customRoles}) + } }, async markSelectedSeriesRead() { await Promise.all(this.selectedSeries.map(s => @@ -532,7 +536,8 @@ export default Vue.extend({ condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)), } as BookSearch))) const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) } else this.$store.dispatch('dialogUpdateSeries', this.selectedSeries) }, @@ -540,9 +545,15 @@ export default Vue.extend({ if (this.selectedBooks.every(b => b.oneshot)) { const series = await Promise.all(this.selectedBooks.map(b => this.$komgaSeries.getOneSeries(b.seriesId))) const oneshots = this.selectedBooks.map((b, index) => ({series: series[index], book: b} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) - } else - this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) + } else { + let customRoles = [] as string[] + if (isAllSelectedSameSeries(this.selectedBooks)) { + customRoles = getCustomRolesForSeries(this.getAllBooksFromLoader(), this.selectedBooks[0].seriesId) + } + this.$store.dispatch('dialogUpdateBooks', {books: this.selectedBooks, roles: customRoles}) + } }, deleteSeries() { this.$store.dispatch('dialogDeleteSeries', this.selectedSeries) @@ -583,6 +594,15 @@ export default Vue.extend({ return undefined } }, + getAllBooksFromLoader(): BookDto[] { + return [ + ...(this.loaderInProgressBooks?.items || []), + ...(this.loaderOnDeckBooks?.items || []), + ...(this.loaderRecentlyReleasedBooks?.items || []), + ...(this.loaderLatestBooks?.items || []), + ...(this.loaderRecentlyReadBooks?.items || []), + ] + }, }, }) diff --git a/komga-webui/src/views/SearchView.vue b/komga-webui/src/views/SearchView.vue index f33304a93..90e9b3507 100644 --- a/komga-webui/src/views/SearchView.vue +++ b/komga-webui/src/views/SearchView.vue @@ -188,6 +188,7 @@ import { SearchOperatorIsFalse, SeriesSearch, } from '@/types/komga-search' +import {isAllSelectedSameSeries, getCustomRolesForSeries, getCustomRoles} from '@/functions/author-roles' export default Vue.extend({ name: 'SearchView', @@ -317,16 +318,18 @@ export default Vue.extend({ const book = (await this.$komgaBooks.getBooksList({ condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)), } as BookSearch)).content[0] - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) } else this.$store.dispatch('dialogUpdateSeries', series) }, async singleEditBook(book: BookDto) { if (book.oneshot) { const series = (await this.$komgaSeries.getOneSeries(book.seriesId)) - this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book}) - } else - this.$store.dispatch('dialogUpdateBooks', book) + this.$store.dispatch('dialogUpdateOneshots', {oneshots: {series: series, book: book}}) + } else { + const customRole = getCustomRolesForSeries(this.loaderBooks?.items || [], book.seriesId) + this.$store.dispatch('dialogUpdateBooks', {books: book, roles: customRole}) + } }, singleEditCollection(collection: CollectionDto) { this.$store.dispatch('dialogEditCollection', collection) @@ -372,7 +375,8 @@ export default Vue.extend({ condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)), } as BookSearch))) const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) } else this.$store.dispatch('dialogUpdateSeries', this.selectedSeries) }, @@ -380,9 +384,15 @@ export default Vue.extend({ if (this.selectedBooks.every(b => b.oneshot)) { const series = await Promise.all(this.selectedBooks.map(b => this.$komgaSeries.getOneSeries(b.seriesId))) const oneshots = this.selectedBooks.map((b, index) => ({series: series[index], book: b} as Oneshot)) - this.$store.dispatch('dialogUpdateOneshots', oneshots) - } else - this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) + const customRole = getCustomRoles(oneshots.map(o => o.book)) + this.$store.dispatch('dialogUpdateOneshots', {oneshots, roles: customRole}) + } else { + let customRole = [] as string[] + if (isAllSelectedSameSeries(this.selectedBooks)) { + customRole = getCustomRolesForSeries(this.loaderBooks?.items || [], this.selectedBooks[0].seriesId) + } + this.$store.dispatch('dialogUpdateBooks', {books: this.selectedBooks, roles: customRole}) + } }, bulkEditMultipleBooks() { this.$store.dispatch('dialogUpdateBulkBooks', this.selectedBooks)