diff --git a/komga-webui/src/components/ReusableDialogs.vue b/komga-webui/src/components/ReusableDialogs.vue
index 8b68924a61d..0494bc10573 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) {
@@ -548,7 +553,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 c8b364c8684..5eae4406350 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 00000000000..9767c4de5ad
--- /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 fccf71ef3b2..5772d565380 100644
--- a/komga-webui/src/store.ts
+++ b/komga-webui/src/store.ts
@@ -40,6 +40,7 @@ export default new Vuex.Store({
updateBooksDialog: false,
deleteBooks: {} as BookDto | BookDto[],
deleteBookDialog: false,
+ updateBooksAdditionalRoles: [] as string[],
// books bulk
updateBulkBooks: [] as BookDto[],
updateBulkBooksDialog: false,
@@ -47,6 +48,7 @@ export default new Vuex.Store({
// oneshots
updateOneshots: {} as Oneshot | Oneshot[],
updateOneshotsDialog: false,
+ updateOneshotsAdditionalRoles: [] as string[],
// series
updateSeries: {} as SeriesDto | SeriesDto[],
@@ -134,6 +136,9 @@ export default new Vuex.Store({
setUpdateBulkBooks(state, books) {
state.updateBulkBooks = books
},
+ setUpdateBooksAdditionalRoles(state, roles) {
+ state.updateBooksAdditionalRoles = roles
+ },
setUpdateBulkBooksDialog(state, dialog) {
state.updateBulkBooksDialog = dialog
},
@@ -144,6 +149,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
@@ -229,8 +237,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) {
@@ -252,8 +261,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 33aa5312d5b..5da5a0446fe 100644
--- a/komga-webui/src/views/BrowseBook.vue
+++ b/komga-webui/src/views/BrowseBook.vue
@@ -408,6 +408,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 {getBookFormatFromMediaType, getBookReadRouteFromMediaProfile} from '@/functions/book-format'
import {getPagesLeft, getReadProgress, getReadProgressPercentage} from '@/functions/book-progress'
@@ -648,7 +649,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})
},
},
})
diff --git a/komga-webui/src/views/BrowseCollection.vue b/komga-webui/src/views/BrowseCollection.vue
index 05b98f90c10..37075fd4494 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'
@@ -493,7 +494,7 @@ export default Vue.extend({
async editSingleSeries(series: SeriesDto) {
if (series.oneshot) {
const book = (await this.$komgaSeries.getBooks(series.id)).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)
},
@@ -501,7 +502,8 @@ export default Vue.extend({
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
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 703afabca0a..9f87e0c18df 100644
--- a/komga-webui/src/views/BrowseLibraries.vue
+++ b/komga-webui/src/views/BrowseLibraries.vue
@@ -131,6 +131,7 @@ 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 {getCustomRoles} from '@/functions/author-roles'
import {parseBooleanFilter, parseQuerySort} from '@/functions/query-params'
import {ReadStatus} from '@/types/enum-books'
import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
@@ -589,7 +590,7 @@ export default Vue.extend({
async editSingleSeries(series: SeriesDto) {
if (series.oneshot) {
const book = (await this.$komgaSeries.getBooks(series.id)).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)
},
@@ -597,7 +598,8 @@ export default Vue.extend({
if (this.selectedOneshots) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
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 a8865d985d0..6f710abfb71 100644
--- a/komga-webui/src/views/BrowseOneshot.vue
+++ b/komga-webui/src/views/BrowseOneshot.vue
@@ -784,7 +784,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})
},
},
})
diff --git a/komga-webui/src/views/BrowseReadList.vue b/komga-webui/src/views/BrowseReadList.vue
index 118613d74c2..a483051a5b1 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'
@@ -478,20 +479,11 @@ export default Vue.extend({
reloadPage: throttle(function (this: any) {
this.loadPage(this.readListId, this.page)
}, 1000),
- async editSingleBook(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)
- },
- 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)
- } else
- this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
+ editSingleBook(book: BookDto) {
+ this.$store.dispatch('dialogUpdateBooks', book)
+ },
+ editMultipleBooks() {
+ this.$store.dispatch('dialogUpdateBooks', 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 b0ecbdedab5..1db7003576c 100644
--- a/komga-webui/src/views/BrowseSeries.vue
+++ b/komga-webui/src/views/BrowseSeries.vue
@@ -485,6 +485,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 {ReadStatus} from '@/types/enum-books'
@@ -911,10 +912,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 f889cf07dd0..05e7d0e2d55 100644
--- a/komga-webui/src/views/DashboardView.vue
+++ b/komga-webui/src/views/DashboardView.vue
@@ -232,6 +232,7 @@ import {BookSseDto, ReadProgressSeriesSseDto, ReadProgressSseDto, SeriesSseDto}
import {LibraryDto} from '@/types/komga-libraries'
import {PageLoader} from '@/types/pageLoader'
import {ItemContext} from '@/types/items'
+import {getCustomRoles, getCustomRolesForSeries, isAllSelectedSameSeries} from '@/functions/author-roles'
export default Vue.extend({
name: 'DashboardView',
@@ -430,16 +431,19 @@ export default Vue.extend({
async singleEditSeries(series: SeriesDto) {
if (series.oneshot) {
let book = (await this.$komgaSeries.getBooks(series.id)).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 =>
@@ -465,7 +469,8 @@ export default Vue.extend({
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
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)
},
@@ -473,9 +478,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)
@@ -509,6 +520,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 b95060b2e79..593ee925880 100644
--- a/komga-webui/src/views/SearchView.vue
+++ b/komga-webui/src/views/SearchView.vue
@@ -180,6 +180,7 @@ import {throttle} from 'lodash'
import {PageLoader} from '@/types/pageLoader'
import {ItemContext} from '@/types/items'
import {ReadListDto} from '@/types/komga-readlists'
+import {isAllSelectedSameSeries, getCustomRolesForSeries, getCustomRoles} from '@/functions/author-roles'
export default Vue.extend({
name: 'SearchView',
@@ -307,16 +308,18 @@ export default Vue.extend({
async singleEditSeries(series: SeriesDto) {
if (series.oneshot) {
let book = (await this.$komgaSeries.getBooks(series.id)).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)
@@ -353,7 +356,8 @@ export default Vue.extend({
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
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)
},
@@ -361,9 +365,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)