diff --git a/komga-webui/src/components/FileImportRow.vue b/komga-webui/src/components/FileImportRow.vue
index 8bebfee3..a3758521 100644
--- a/komga-webui/src/components/FileImportRow.vue
+++ b/komga-webui/src/components/FileImportRow.vue
@@ -31,7 +31,7 @@
-
+
diff --git a/komga-webui/src/components/ItemCard.vue b/komga-webui/src/components/ItemCard.vue
index ec19c3eb..efea3b8c 100644
--- a/komga-webui/src/components/ItemCard.vue
+++ b/komga-webui/src/components/ItemCard.vue
@@ -20,7 +20,7 @@
-
@@ -72,11 +72,19 @@
-
+
-
+
@@ -128,7 +136,7 @@
-
+
@@ -166,10 +174,11 @@ import {
} from '@/types/komga-sse'
import {coverBase64} from '@/types/image'
import {ReadListDto} from '@/types/komga-readlists'
+import OneShotActionsMenu from '@/components/menus/OneshotActionsMenu.vue'
export default Vue.extend({
name: 'ItemCard',
- components: {BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu, ReadListActionsMenu},
+ components: {OneShotActionsMenu, BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu, ReadListActionsMenu},
props: {
item: {
type: Object as () => BookDto | SeriesDto | CollectionDto | ReadListDto,
@@ -291,6 +300,7 @@ export default Vue.extend({
},
isUnread(): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.UNREAD
+ if (this.computedItem.type() === ItemTypes.SERIES && (this.item as SeriesDto).oneshot) return (this.item as SeriesDto).booksUnreadCount + (this.item as SeriesDto).booksInProgressCount > 0
return false
},
unreadCount(): number | undefined {
diff --git a/komga-webui/src/components/ReusableDialogs.vue b/komga-webui/src/components/ReusableDialogs.vue
index ef5f1ebe..8b68924a 100644
--- a/komga-webui/src/components/ReusableDialogs.vue
+++ b/komga-webui/src/components/ReusableDialogs.vue
@@ -65,6 +65,11 @@
:books="updateBulkBooks"
/>
+
+
{{ data.item.metadata.title }}
- {{ data.item.seriesTitle }} - {{ data.item.metadata.number }}
+ {{ data.item.seriesTitle }} - {{
+ data.item.metadata.number
+ }}
+
{{
$t('searchbox.in_library', {library: getLibraryName(data.item)})
}}
@@ -144,6 +147,10 @@ export default Vue.extend({
})
if (val.type === 'series') this.$router.push({name: 'browse-series', params: {seriesId: val.id}})
+ else if (val.type === 'book' && val.oneshot) this.$router.push({
+ name: 'browse-oneshot',
+ params: {seriesId: val.seriesId},
+ })
else if (val.type === 'book') this.$router.push({name: 'browse-book', params: {bookId: val.id}})
else if (val.type === 'collection') this.$router.push({
name: 'browse-collection',
diff --git a/komga-webui/src/components/bars/MultiSelectBar.vue b/komga-webui/src/components/bars/MultiSelectBar.vue
index aeed951b..7596f653 100644
--- a/komga-webui/src/components/bars/MultiSelectBar.vue
+++ b/komga-webui/src/components/bars/MultiSelectBar.vue
@@ -43,7 +43,7 @@
-
+
mdi-playlist-plus
@@ -52,7 +52,7 @@
-
+
mdi-book-plus-multiple
@@ -114,6 +114,10 @@ export default Vue.extend({
type: String,
required: true,
},
+ oneshots: {
+ type: Boolean,
+ default: false,
+ },
showSelectAll: {
type: Boolean,
default: false,
diff --git a/komga-webui/src/components/dialogs/EditOneshotDialog.vue b/komga-webui/src/components/dialogs/EditOneshotDialog.vue
new file mode 100644
index 00000000..c8b364c8
--- /dev/null
+++ b/komga-webui/src/components/dialogs/EditOneshotDialog.vue
@@ -0,0 +1,1128 @@
+
+
+
+
+
+
+
+
+
diff --git a/komga-webui/src/components/dialogs/LibraryEditDialog.vue b/komga-webui/src/components/dialogs/LibraryEditDialog.vue
index a97a1c5a..8cff1d12 100644
--- a/komga-webui/src/components/dialogs/LibraryEditDialog.vue
+++ b/komga-webui/src/components/dialogs/LibraryEditDialog.vue
@@ -108,6 +108,22 @@
+
+
+
+
+
+ mdi-help-circle-outline
+
+ {{ $t('dialog.edit_library.tooltip_oneshotsdirectory') }}
+
+
+
@@ -392,6 +408,7 @@ export default Vue.extend({
hashFiles: true,
hashPages: false,
analyzeDimensions: true,
+ oneshotsDirectory: '',
},
validationFieldNames: new Map([]),
}
@@ -524,6 +541,7 @@ export default Vue.extend({
this.form.hashFiles = library ? library.hashFiles : true
this.form.hashPages = library ? library.hashPages : false
this.form.analyzeDimensions = library ? library.analyzeDimensions : true
+ this.form.oneshotsDirectory = library ? library.oneshotsDirectory : ''
this.$v.$reset()
},
validateLibrary() {
@@ -551,6 +569,7 @@ export default Vue.extend({
hashFiles: this.form.hashFiles,
hashPages: this.form.hashPages,
analyzeDimensions: this.form.analyzeDimensions,
+ oneshotsDirectory: this.form.oneshotsDirectory,
}
}
return null
diff --git a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
index 374a8874..68c49d0a 100644
--- a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
+++ b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
@@ -100,6 +100,10 @@ export default Vue.extend({
type: Object as PropType,
required: false,
},
+ includeOneshots: {
+ type: Boolean,
+ default: true,
+ },
},
watch: {
value(val) {
@@ -119,7 +123,7 @@ export default Vue.extend({
searchItems: debounce(async function (this: any, query: string) {
if (query) {
this.showResults = false
- this.results = (await this.$komgaSeries.getSeries(undefined, {unpaged: true}, query)).content
+ this.results = (await this.$komgaSeries.getSeries(undefined, {unpaged: true}, query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, this.includeOneshots)).content
this.showResults = true
} else {
this.clear()
diff --git a/komga-webui/src/components/menus/OneshotActionsMenu.vue b/komga-webui/src/components/menus/OneshotActionsMenu.vue
new file mode 100644
index 00000000..0773e56a
--- /dev/null
+++ b/komga-webui/src/components/menus/OneshotActionsMenu.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+ mdi-dots-vertical
+
+
+
+
+ {{ $t('menu.analyze') }}
+
+
+ {{ $t('menu.refresh_metadata') }}
+
+
+ {{ $t('menu.add_to_collection') }}
+
+
+ {{ $t('menu.add_to_readlist') }}
+
+
+ {{ $t('menu.mark_read') }}
+
+
+ {{ $t('menu.mark_unread') }}
+
+
+ {{ $t('menu.delete') }}
+
+
+
+
+
+
diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json
index 70a42de1..d9deaa39 100644
--- a/komga-webui/src/locales/en.json
+++ b/komga-webui/src/locales/en.json
@@ -222,6 +222,7 @@
"lock_all": "Lock all",
"n_selected": "{count} selected",
"nothing_to_show": "Nothing to show",
+ "oneshot": "One-shot",
"outdated": "Outdated",
"page": "Page",
"page_number": "Page number",
@@ -428,6 +429,7 @@
"field_import_local_artwork": "Local artwork",
"field_import_mylar_series": "Series metadata",
"field_name": "Name",
+ "field_oneshotsdirectory": "One-Shots directory",
"field_repair_extensions": "Automatically repair incorrect file extensions",
"field_root_folder": "Root folder",
"field_scanner_empty_trash_after_scan": "Empty trash automatically after every scan",
@@ -447,6 +449,7 @@
"tab_general": "General",
"tab_metadata": "Metadata",
"tab_options": "Options",
+ "tooltip_oneshotsdirectory": "Leave empty to disable",
"tooltip_scanner_force_modified_time": "Enable if the library is on a Google Drive",
"tooltip_use_resources": "Can consume lots of resources on large libraries or slow hardware"
},
@@ -698,6 +701,7 @@
"in_progress": "In Progress",
"language": "language",
"library": "library",
+ "oneshot": "One-shot",
"publisher": "publisher",
"read": "Read",
"release_date": "release date",
diff --git a/komga-webui/src/router.ts b/komga-webui/src/router.ts
index d9d870b5..8886e9e2 100644
--- a/komga-webui/src/router.ts
+++ b/komga-webui/src/router.ts
@@ -213,6 +213,12 @@ const router = new Router({
component: () => import(/* webpackChunkName: "browse-book" */ './views/BrowseBook.vue'),
props: (route) => ({bookId: route.params.bookId}),
},
+ {
+ path: '/oneshot/:seriesId',
+ name: 'browse-oneshot',
+ component: () => import(/* webpackChunkName: "browse-oneshot" */ './views/BrowseOneshot.vue'),
+ props: (route) => ({seriesId: route.params.seriesId}),
+ },
{
path: '/search',
name: 'search',
diff --git a/komga-webui/src/services/komga-libraries.service.ts b/komga-webui/src/services/komga-libraries.service.ts
index ff6c7203..85188906 100644
--- a/komga-webui/src/services/komga-libraries.service.ts
+++ b/komga-webui/src/services/komga-libraries.service.ts
@@ -48,7 +48,7 @@ export default class KomgaLibrariesService {
async updateLibrary(libraryId: string, library: LibraryUpdateDto) {
try {
- await this.http.put(`${API_LIBRARIES}/${libraryId}`, library)
+ await this.http.patch(`${API_LIBRARIES}/${libraryId}`, library)
} catch (e) {
let msg = `An error occurred while trying to update library '${libraryId}'`
if (e.response.data.message) {
diff --git a/komga-webui/src/services/komga-series.service.ts b/komga-webui/src/services/komga-series.service.ts
index dae82bcb..49989adc 100644
--- a/komga-webui/src/services/komga-series.service.ts
+++ b/komga-webui/src/services/komga-series.service.ts
@@ -16,7 +16,7 @@ export default class KomgaSeriesService {
async getSeries(libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[],
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
- searchRegex?: string, complete?: boolean, sharingLabel?: string[]): Promise> {
+ searchRegex?: string, complete?: boolean, sharingLabel?: string[], oneshot?: boolean): Promise> {
try {
const params = {...pageRequest} as any
if (libraryId) params.library_id = libraryId
@@ -33,6 +33,7 @@ export default class KomgaSeriesService {
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
if (complete !== undefined) params.complete = complete
if (sharingLabel) params.sharing_label = sharingLabel
+ if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(API_SERIES, {
params: params,
@@ -50,7 +51,7 @@ export default class KomgaSeriesService {
async getAlphabeticalGroups(libraryId?: string, search?: string, status?: string[],
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
- complete?: boolean, sharingLabel?: string[]): Promise {
+ complete?: boolean, sharingLabel?: string[], oneshot?: boolean): Promise {
try {
const params = {} as any
if (libraryId) params.library_id = libraryId
@@ -66,6 +67,7 @@ export default class KomgaSeriesService {
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
if (complete !== undefined) params.complete = complete
if (sharingLabel) params.sharing_label = sharingLabel
+ if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(`${API_SERIES}/alphabetical-groups`, {
params: params,
@@ -80,12 +82,11 @@ export default class KomgaSeriesService {
}
}
- async getNewSeries(libraryId?: string, pageRequest?: PageRequest): Promise> {
+ async getNewSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise> {
try {
const params = {...pageRequest} as any
- if (libraryId) {
- params.library_id = libraryId
- }
+ if (libraryId) params.library_id = libraryId
+ if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(`${API_SERIES}/new`, {
params: params,
})).data
@@ -98,12 +99,11 @@ export default class KomgaSeriesService {
}
}
- async getUpdatedSeries(libraryId?: string, pageRequest?: PageRequest): Promise> {
+ async getUpdatedSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise> {
try {
const params = {...pageRequest} as any
- if (libraryId) {
- params.library_id = libraryId
- }
+ if (libraryId) params.library_id = libraryId
+ if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(`${API_SERIES}/updated`, {
params: params,
})).data
diff --git a/komga-webui/src/store.ts b/komga-webui/src/store.ts
index f286fe7d..fccf71ef 100644
--- a/komga-webui/src/store.ts
+++ b/komga-webui/src/store.ts
@@ -1,7 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import {BookDto} from '@/types/komga-books'
-import {SeriesDto} from '@/types/komga-series'
+import {Oneshot, SeriesDto} from '@/types/komga-series'
import createPersistedState from 'vuex-persistedstate'
import {persistedModule} from './plugins/persisted-state'
import {LibraryDto} from '@/types/komga-libraries'
@@ -44,6 +44,10 @@ export default new Vuex.Store({
updateBulkBooks: [] as BookDto[],
updateBulkBooksDialog: false,
+ // oneshots
+ updateOneshots: {} as Oneshot | Oneshot[],
+ updateOneshotsDialog: false,
+
// series
updateSeries: {} as SeriesDto | SeriesDto[],
updateSeriesDialog: false,
@@ -133,6 +137,13 @@ export default new Vuex.Store({
setUpdateBulkBooksDialog(state, dialog) {
state.updateBulkBooksDialog = dialog
},
+ // One-shots
+ setUpdateOneshots(state, oneshots) {
+ state.updateOneshots = oneshots
+ },
+ setUpdateOneshotsDialog(state, dialog) {
+ state.updateOneshotsDialog = dialog
+ },
// Series
setUpdateSeries(state, series) {
state.updateSeries = series
@@ -240,6 +251,15 @@ export default new Vuex.Store({
dialogUpdateBulkBooksDisplay({commit}, value) {
commit('setUpdateBulkBooksDialog', value)
},
+ // oneshots
+ dialogUpdateOneshots({commit}, oneshots) {
+ commit('setUpdateOneshots', oneshots)
+ commit('setUpdateOneshotsDialog', true)
+ },
+ dialogUpdateOneshotsDisplay({commit}, value) {
+ commit('setUpdateOneshotsDialog', value)
+ },
+
// series
dialogUpdateSeries({commit}, series) {
commit('setUpdateSeries', series)
diff --git a/komga-webui/src/types/items.ts b/komga-webui/src/types/items.ts
index 6b81a7e0..08aa0e37 100644
--- a/komga-webui/src/types/items.ts
+++ b/komga-webui/src/types/items.ts
@@ -77,6 +77,11 @@ export class BookItem extends Item {
title(context: ItemContext[]): ItemTitle | ItemTitle[] {
+ if (this.item.oneshot)
+ return {
+ title: this.item.metadata.title,
+ to: this.to(),
+ }
if (context.includes(ItemContext.SHOW_SERIES))
return [
{
@@ -107,16 +112,23 @@ export class BookItem extends Item {
let text
let title
if (context.includes(ItemContext.RELEASE_DATE))
- text = this.item.metadata.releaseDate ? new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium', timeZone: 'UTC'} as Intl.DateTimeFormatOptions).format(new Date(this.item.metadata.releaseDate)) : i18n.t('book_card.no_release_date')
- else if (context.includes(ItemContext.DATE_ADDED))
- {
- text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
- title = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'long', timeStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
- }
- else if (context.includes(ItemContext.READ_DATE)) {
+ text = this.item.metadata.releaseDate ? new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'medium',
+ timeZone: 'UTC',
+ } as Intl.DateTimeFormatOptions).format(new Date(this.item.metadata.releaseDate)) : i18n.t('book_card.no_release_date')
+ else if (context.includes(ItemContext.DATE_ADDED)) {
+ text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
+ title = new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'long',
+ timeStyle: 'medium',
+ } as Intl.DateTimeFormatOptions).format(this.item.created)
+ } else if (context.includes(ItemContext.READ_DATE)) {
if (this.item.readProgress?.readDate) {
text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.readProgress?.readDate)
- title = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'long', timeStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.readProgress?.readDate)
+ title = new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'long',
+ timeStyle: 'medium',
+ } as Intl.DateTimeFormatOptions).format(this.item.readProgress?.readDate)
} else {
text = i18n.t('book_card.unread')
}
@@ -124,23 +136,35 @@ export class BookItem extends Item {
text = getFileSize(this.item.sizeBytes)
else
text = i18n.tc('common.pages_n', this.item.media.pagesCount)
- return `${text}
`
+ return `${text}
`
}
}
to(): RawLocation {
- return {
- name: 'browse-book',
- params: {bookId: this.item.id},
- query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
- }
+ return this.item.oneshot ?
+ {
+ name: 'browse-oneshot',
+ params: {seriesId: this.item.seriesId},
+ query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
+ } :
+ {
+ name: 'browse-book',
+ params: {bookId: this.item.id},
+ query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
+ }
}
seriesTo(): RawLocation {
- return {
- name: 'browse-series',
- params: {seriesId: this.item.seriesId},
- }
+ return this.item.oneshot ?
+ {
+ name: 'browse-oneshot',
+ params: {seriesId: this.item.seriesId},
+ query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
+ } :
+ {
+ name: 'browse-series',
+ params: {seriesId: this.item.seriesId},
+ }
}
fabTo(): RawLocation {
@@ -174,25 +198,32 @@ export class SeriesItem extends Item {
let text
let title
if (context.includes(ItemContext.RELEASE_DATE))
- text = this.item.booksMetadata.releaseDate ? new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium', timeZone: 'UTC'} as Intl.DateTimeFormatOptions).format(new Date(this.item.booksMetadata.releaseDate)) : i18n.t('book_card.no_release_date')
- else if (context.includes(ItemContext.DATE_ADDED))
- {
- text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
- title = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'long', timeStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
- }
- else if (context.includes(ItemContext.DATE_UPDATED))
- {
- text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.lastModified)
- title = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'long', timeStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.lastModified)
- }
- else
+ text = this.item.booksMetadata.releaseDate ? new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'medium',
+ timeZone: 'UTC',
+ } as Intl.DateTimeFormatOptions).format(new Date(this.item.booksMetadata.releaseDate)) : i18n.t('book_card.no_release_date')
+ else if (context.includes(ItemContext.DATE_ADDED)) {
+ text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.created)
+ title = new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'long',
+ timeStyle: 'medium',
+ } as Intl.DateTimeFormatOptions).format(this.item.created)
+ } else if (context.includes(ItemContext.DATE_UPDATED)) {
+ text = new Intl.DateTimeFormat(i18n.locale, {dateStyle: 'medium'} as Intl.DateTimeFormatOptions).format(this.item.lastModified)
+ title = new Intl.DateTimeFormat(i18n.locale, {
+ dateStyle: 'long',
+ timeStyle: 'medium',
+ } as Intl.DateTimeFormatOptions).format(this.item.lastModified)
+ } else if (this.item.oneshot) {
+ text = i18n.t('common.oneshot')
+ } else
text = i18n.tc('common.books_n', this.item.booksCount)
- return `${text}
`
+ return `${text}
`
}
to(): RawLocation {
return {
- name: 'browse-series', params: {seriesId: this.item.id.toString()},
+ name: this.item.oneshot ? 'browse-oneshot' : 'browse-series', params: {seriesId: this.item.id.toString()},
query: {context: this.item?.context?.origin, contextId: this.item?.context?.id},
}
}
diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts
index 0e308a96..a478fa94 100644
--- a/komga-webui/src/types/komga-books.ts
+++ b/komga-webui/src/types/komga-books.ts
@@ -17,6 +17,7 @@ export interface BookDto {
metadata: BookMetadataDto,
readProgress?: ReadProgressDto,
deleted: boolean,
+ oneshot: boolean,
// custom fields
context: Context
diff --git a/komga-webui/src/types/komga-libraries.ts b/komga-webui/src/types/komga-libraries.ts
index 69dacef0..907378c3 100644
--- a/komga-webui/src/types/komga-libraries.ts
+++ b/komga-webui/src/types/komga-libraries.ts
@@ -21,6 +21,7 @@ export interface LibraryCreationDto {
hashFiles: boolean,
hashPages: boolean,
analyzeDimensions: boolean,
+ oneshotsDirectory: string,
}
export interface LibraryUpdateDto {
@@ -44,6 +45,7 @@ export interface LibraryUpdateDto {
hashFiles: boolean,
hashPages: boolean,
analyzeDimensions: boolean,
+ oneshotsDirectory: string,
}
export interface LibraryDto {
@@ -68,5 +70,6 @@ export interface LibraryDto {
hashFiles: boolean,
hashPages: boolean,
analyzeDimensions: boolean,
+ oneshotsDirectory: string,
unavailable: boolean,
}
diff --git a/komga-webui/src/types/komga-series.ts b/komga-webui/src/types/komga-series.ts
index f3669251..dfea1b47 100644
--- a/komga-webui/src/types/komga-series.ts
+++ b/komga-webui/src/types/komga-series.ts
@@ -1,4 +1,4 @@
-import {AuthorDto, WebLinkDto} from '@/types/komga-books'
+import {AuthorDto, BookDto, WebLinkDto} from '@/types/komga-books'
import {Context} from '@/types/context'
export interface SeriesDto {
@@ -15,6 +15,7 @@ export interface SeriesDto {
metadata: SeriesMetadataDto,
booksMetadata: SeriesBooksMetadataDto,
deleted: boolean,
+ oneshot: boolean,
// custom fields
context: Context
@@ -110,3 +111,8 @@ export interface AlternateTitleDto {
label: string,
title: string
}
+
+export interface Oneshot {
+ series: SeriesDto,
+ book: BookDto,
+}
diff --git a/komga-webui/src/views/BookReader.vue b/komga-webui/src/views/BookReader.vue
index 7b080339..624071aa 100644
--- a/komga-webui/src/views/BookReader.vue
+++ b/komga-webui/src/views/BookReader.vue
@@ -69,7 +69,7 @@
{{ $t('bookreader.set_current_page_as_series_poster') }}
-
+
{{ $t('bookreader.set_current_page_as_readlist_poster') }}
@@ -780,11 +780,12 @@ export default Vue.extend({
} as Location)
},
closeBook() {
- this.$router.push({
- name: 'browse-book',
- params: {bookId: this.bookId.toString()},
- query: {context: this.context.origin, contextId: this.context.id},
- })
+ this.$router.push(
+ {
+ name: this.book.oneshot ? 'browse-oneshot' : 'browse-book',
+ params: {bookId: this.bookId.toString(), seriesId: this.book.seriesId},
+ query: {context: this.context.origin, contextId: this.context.id},
+ })
},
changeReadingDir(dir: ReadingDirection) {
this.readingDirection = dir
diff --git a/komga-webui/src/views/BrowseBook.vue b/komga-webui/src/views/BrowseBook.vue
index a29e8382..0c706573 100644
--- a/komga-webui/src/views/BrowseBook.vue
+++ b/komga-webui/src/views/BrowseBook.vue
@@ -40,7 +40,7 @@
@@ -64,12 +64,13 @@
- {{ book.seriesTitle }} {{ book.metadata.number }}:
+ {{ book.seriesTitle }} {{ book.metadata.number }}:
{{ book.metadata.title }}
+ {{ book.metadata.title }}
{{ book.metadata.number }} - {{ book.metadata.title }}
@@ -81,7 +82,7 @@
@@ -159,7 +160,10 @@
{{
- new Intl.DateTimeFormat($i18n.locale, {dateStyle: 'long', timeZone: 'UTC'}).format(new Date(book.metadata.releaseDate))
+ new Intl.DateTimeFormat($i18n.locale, {
+ dateStyle: 'long',
+ timeZone: 'UTC'
+ }).format(new Date(book.metadata.releaseDate))
}}
@@ -581,6 +585,11 @@ export default Vue.extend({
},
async loadBook(bookId: string) {
this.book = await this.$komgaBooks.getBook(bookId)
+ // for the cases where we can't change the origin target route because we don't have the full BookDto
+ if (this.book.oneshot) await this.$router.replace({
+ name: 'browse-oneshot',
+ params: {seriesId: this.book.seriesId},
+ })
// parse query params to get context and contextId
if (this.$route.query.contextId && this.$route.query.context
diff --git a/komga-webui/src/views/BrowseCollection.vue b/komga-webui/src/views/BrowseCollection.vue
index 9e56d9ab..1571e5de 100644
--- a/komga-webui/src/views/BrowseCollection.vue
+++ b/komga-webui/src/views/BrowseCollection.vue
@@ -47,12 +47,14 @@
@@ -154,9 +156,9 @@ import FilterPanels from '@/components/FilterPanels.vue'
import FilterList from '@/components/FilterList.vue'
import {Location} from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
-import {SeriesDto} from '@/types/komga-series'
+import {Oneshot, SeriesDto} from '@/types/komga-series'
import {authorRoles} from '@/types/author-roles'
-import {AuthorDto, BookDto} from '@/types/komga-books'
+import {AuthorDto} from '@/types/komga-books'
import {CollectionSseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
import {throttle} from 'lodash'
import {LibraryDto} from '@/types/komga-libraries'
@@ -322,6 +324,9 @@ export default Vue.extend({
filterActive(): boolean {
return Object.keys(this.filters).some(x => this.filters[x].length !== 0)
},
+ selectedOneshots(): boolean {
+ return this.selectedSeries.every(s => s.oneshot)
+ },
},
methods: {
resetFilters() {
@@ -503,11 +508,20 @@ export default Vue.extend({
this.$router.replace(loc).catch((_: any) => {
})
},
- editSingleSeries(series: SeriesDto) {
- this.$store.dispatch('dialogUpdateSeries', series)
+ 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})
+ } else
+ this.$store.dispatch('dialogUpdateSeries', series)
},
- editMultipleSeries() {
- this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ async editMultipleSeries() {
+ 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)
+ } else
+ this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
},
deleteSeries() {
this.$store.dispatch('dialogDeleteSeries', this.selectedSeries)
@@ -527,6 +541,10 @@ export default Vue.extend({
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries.map(s => s.id))
},
+ async addToReadList() {
+ const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
+ this.$store.dispatch('dialogAddBooksToReadList', books.map(b => b.content[0].id))
+ },
async startEditElements() {
this.filters = {}
this.unpaged = true
diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue
index 72b07214..7f17d116 100644
--- a/komga-webui/src/views/BrowseLibraries.vue
+++ b/komga-webui/src/views/BrowseLibraries.vue
@@ -28,12 +28,14 @@
@@ -149,7 +151,7 @@ import SortList from '@/components/SortList.vue'
import FilterPanels from '@/components/FilterPanels.vue'
import FilterList from '@/components/FilterList.vue'
import {mergeFilterParams, sortOrFilterActive, toNameValue} from '@/functions/filter'
-import {GroupCountDto, SeriesDto} from '@/types/komga-series'
+import {GroupCountDto, Oneshot, SeriesDto} from '@/types/komga-series'
import {AuthorDto} from '@/types/komga-books'
import {authorRoles} from '@/types/author-roles'
import {LibrarySseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
@@ -265,9 +267,9 @@ export default Vue.extend({
},
computed: {
searchRegex(): string | undefined {
- if (this.selectedSymbol === 'ALL') return undefined
- if (this.selectedSymbol === '#') return '^[^a-z],title_sort'
- return `^${this.selectedSymbol},title_sort`
+ if (this.selectedSymbol === 'ALL') return undefined
+ if (this.selectedSymbol === '#') return '^[^a-z],title_sort'
+ return `^${this.selectedSymbol},title_sort`
},
itemContext(): ItemContext[] {
if (this.sortActive.key === 'booksMetadata.releaseDate') return [ItemContext.RELEASE_DATE]
@@ -297,6 +299,9 @@ export default Vue.extend({
complete: {
values: [{name: this.$t('filter.complete').toString(), value: 'true', nValue: 'false'}],
},
+ oneshot: {
+ values: [{name: this.$t('filter.oneshot').toString(), value: 'true', nValue: 'false'}],
+ },
} as FiltersOptions
},
filterOptionsPanel(): FiltersOptions {
@@ -348,6 +353,9 @@ export default Vue.extend({
sortOrFilterActive(): boolean {
return sortOrFilterActive(this.sortActive, this.sortDefault, this.filters)
},
+ selectedOneshots(): boolean {
+ return this.selectedSeries.every(s => s.oneshot)
+ },
},
methods: {
filterByStarting(symbol: string) {
@@ -393,7 +401,7 @@ export default Vue.extend({
// get filter from query params or local storage and validate with available filter values
let activeFilters: any
- if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.publisher || authorRoles.some(role => role in route.query) || route.query.complete || route.query.sharingLabel) {
+ if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.publisher || authorRoles.some(role => role in route.query) || route.query.complete || route.query.oneshot || route.query.sharingLabel) {
activeFilters = {
status: route.query.status || [],
readStatus: route.query.readStatus || [],
@@ -404,6 +412,7 @@ export default Vue.extend({
ageRating: route.query.ageRating || [],
releaseDate: route.query.releaseDate || [],
complete: route.query.complete || [],
+ oneshot: route.query.oneshot || [],
sharingLabel: route.query.sharingLabel || [],
}
authorRoles.forEach((role: string) => {
@@ -425,6 +434,7 @@ export default Vue.extend({
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [],
+ oneshot: filters.oneshot?.filter(x => x === 'true' || x === 'false') || [],
sharingLabel: filters.sharingLabel?.filter(x => this.filterOptions.sharingLabel.map(n => n.value).includes(x)) || [],
} as any
authorRoles.forEach((role: string) => {
@@ -532,13 +542,14 @@ export default Vue.extend({
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
const complete = parseBooleanFilter(this.filters.complete)
- const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, searchRegex, complete, this.filters.sharingLabel)
+ const oneshot = parseBooleanFilter(this.filters.oneshot)
+ const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, searchRegex, complete, this.filters.sharingLabel, oneshot)
this.totalPages = seriesPage.totalPages
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
- const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete, this.filters.sharingLabel)
+ const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete, this.filters.sharingLabel, oneshot)
const nonAlpha = seriesGroups
.filter((g) => !(/[a-zA-Z]/).test(g.group))
.reduce((a, b) => a + b.count, 0)
@@ -571,11 +582,24 @@ export default Vue.extend({
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries.map(s => s.id))
},
- editSingleSeries(series: SeriesDto) {
- this.$store.dispatch('dialogUpdateSeries', series)
+ async addToReadList() {
+ const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
+ this.$store.dispatch('dialogAddBooksToReadList', books.map(b => b.content[0].id))
},
- editMultipleSeries() {
- this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ 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})
+ } else
+ this.$store.dispatch('dialogUpdateSeries', series)
+ },
+ async editMultipleSeries() {
+ 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)
+ } else
+ this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
},
deleteSeries() {
this.$store.dispatch('dialogDeleteSeries', this.selectedSeries)
diff --git a/komga-webui/src/views/BrowseOneshot.vue b/komga-webui/src/views/BrowseOneshot.vue
new file mode 100644
index 00000000..728b850b
--- /dev/null
+++ b/komga-webui/src/views/BrowseOneshot.vue
@@ -0,0 +1,796 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('common.go_to_readlist') }}
+ {{ $t('common.go_to_collection') }}
+ {{ $t('common.go_to_library') }}
+
+
+
+
+
+
+ mdi-pencil
+
+
+
+
+
+ {{ $t('browse_book.navigation_within_readlist', {name: contextName}) }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-menu
+
+
+
+
+
+
+
+ {{ book.seriesTitle }}
+ {{ book.metadata.number }}:
+ {{ book.metadata.title }}
+
+ {{
+ book.metadata.title
+ }}
+
+ {{ book.metadata.number }} - {{ book.metadata.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('browse_book.navigation_within_readlist', {name: contextName}) }}
+
+
+
+
+
+
+
+ {{ $tc('common.pages_left', pagesLeft) }}
+
+
+ {{ $t('common.read_on', {date: readProgressDate}) }}
+
+
+
+
+
+
+
+ {{ book.metadata.title }}
+ {{ $t('searchbox.in_library', {library: getLibraryName(book)}) }}
+
+
+
+
+
+
+
+ {{ series.metadata.ageRating }}+
+
+
+
+
+ {{ languageDisplay }}
+
+
+
+
+ {{ $t(`enums.reading_direction.${series.metadata.readingDirection}`) }}
+
+
+
+
+ {{ $t('common.unavailable') }}
+
+
+
+
+
+
+ {{ $t('book_card.unknown') }}
+
+
+
+ {{ $tc('common.pages_n', book.media.pagesCount) }}
+
+
+
+ {{
+ new Intl.DateTimeFormat($i18n.locale, {
+ dateStyle: 'long',
+ timeZone: 'UTC'
+ }).format(new Date(book.metadata.releaseDate))
+ }}
+
+
+
+
+
+
+ {{ $t('common.outdated') }}
+
+
+ {{ $t('browse_book.outdated_tooltip') }}
+
+
+
+
+
+ {{ $t('common.unavailable') }}
+
+
+
+
+
+
+
+
+ mdi-book-open-page-variant
+ {{ $t('common.read') }}
+
+
+
+
+
+ mdi-incognito
+ {{ $t('common.read') }}
+
+
+
+
+
+ mdi-file-download
+ {{ $t('common.download') }}
+
+
+
+
+
+
+ {{ book.metadata.summary }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-book-open-page-variant
+ {{ $t('common.read') }}
+
+
+
+
+
+ mdi-incognito
+ {{ $t('common.read') }}
+
+
+
+
+
+ mdi-file-download
+ {{ $t('common.download') }}
+
+
+
+
+
+
+ {{ book.metadata.summary }}
+
+
+
+
+
+
+ {{ $t('common.publisher') }}
+
+ {{ series.metadata.publisher }}
+
+
+
+
+
+
+ {{ $t('common.genre') }}
+
+
+
+
+ mdi-chevron-left
+
+
+
+
+
+ mdi-chevron-right
+
+
+ {{ t }}
+
+
+
+
+
+
+
+ {{ $te(`author_roles.${role}`) ? $t(`author_roles.${role}`) : role }}
+
+
+
+
+
+ mdi-chevron-left
+
+
+
+
+
+ mdi-chevron-right
+
+
+ {{ name }}
+
+
+
+
+
+
+ {{ $i18n.t('common.tags') }}
+
+
+
+
+ mdi-chevron-left
+
+
+
+
+
+ mdi-chevron-right
+
+
+ {{ t }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('browse_book.links') }}
+
+
+ {{ link.label }}
+
+ mdi-open-in-new
+
+
+
+
+
+
+ {{ $t('browse_book.size') }}
+ {{ book.size }}
+
+
+
+ {{ $t('browse_book.comment') }}
+ {{ mediaComment }}
+
+
+
+ {{ $t('browse_book.format') }}
+ {{ format.type }}
+
+
+
+ {{ $t('browse_book.isbn') }}
+ {{ book.metadata.isbn }}
+
+
+
+ {{ $t('browse_book.file') }}
+ {{ book.url }}
+
+
+
+
+
+
+
+
+
+
diff --git a/komga-webui/src/views/BrowseReadList.vue b/komga-webui/src/views/BrowseReadList.vue
index 7d9f19df..458d1757 100644
--- a/komga-webui/src/views/BrowseReadList.vue
+++ b/komga-webui/src/views/BrowseReadList.vue
@@ -47,11 +47,13 @@
this.filters[x].length !== 0)
},
+ selectedOneshots(): boolean {
+ return this.selectedBooks.every(b => b.oneshot)
+ },
},
methods: {
resetFilters() {
@@ -472,11 +478,20 @@ export default Vue.extend({
reloadPage: throttle(function (this: any) {
this.loadPage(this.readListId, this.page)
}, 1000),
- editSingleBook(book: BookDto) {
- this.$store.dispatch('dialogUpdateBooks', book)
+ 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)
},
- editMultipleBooks() {
- this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
+ 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)
},
bulkEditMultipleBooks() {
this.$store.dispatch('dialogUpdateBulkBooks', this.selectedBooks)
@@ -496,6 +511,9 @@ export default Vue.extend({
))
this.selectedBooks = []
},
+ addToCollection() {
+ this.$store.dispatch('dialogAddSeriesToCollection', this.selectedBooks.map(b => b.seriesId))
+ },
addToReadList() {
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks.map(b => b.id))
},
diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue
index 8ae684a6..419b7750 100644
--- a/komga-webui/src/views/BrowseSeries.vue
+++ b/komga-webui/src/views/BrowseSeries.vue
@@ -821,7 +821,11 @@ export default Vue.extend({
}, 1000),
async loadSeries(seriesId: string) {
this.$komgaSeries.getOneSeries(seriesId)
- .then(v => this.series = v)
+ .then(v => {
+ this.series = v
+ // for the cases where we can't change the origin target route because we don't have the full BookDto
+ if (this.series.oneshot) this.$router.replace({name: 'browse-oneshot', params: {seriesId: this.seriesId}})
+ })
this.$komgaSeries.getCollections(seriesId)
.then(v => this.collections = v)
diff --git a/komga-webui/src/views/DashboardView.vue b/komga-webui/src/views/DashboardView.vue
index c7a810c2..f889cf07 100644
--- a/komga-webui/src/views/DashboardView.vue
+++ b/komga-webui/src/views/DashboardView.vue
@@ -34,10 +34,12 @@
b.oneshot)
+ },
},
methods: {
async scrollChanged(loader: PageLoader, percent: number) {
@@ -383,11 +388,11 @@ export default Vue.extend({
this.loaderNewSeries = new PageLoader(
{},
- (pageable: PageRequest) => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId), pageable),
+ (pageable: PageRequest) => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId), false, pageable),
)
this.loaderUpdatedSeries = new PageLoader(
{},
- (pageable: PageRequest) => this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId), pageable),
+ (pageable: PageRequest) => this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId), false, pageable),
)
},
loadAll(libraryId: string, reload: boolean = false) {
@@ -422,11 +427,19 @@ export default Vue.extend({
})
}
},
- singleEditSeries(series: SeriesDto) {
- this.$store.dispatch('dialogUpdateSeries', series)
+ 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('dialogUpdateSeries', series)
},
- singleEditBook(book: BookDto) {
- this.$store.dispatch('dialogUpdateBooks', book)
+ 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)
},
async markSelectedSeriesRead() {
await Promise.all(this.selectedSeries.map(s =>
@@ -444,11 +457,25 @@ export default Vue.extend({
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries.map(s => s.id))
this.selectedSeries = []
},
- editMultipleSeries() {
- this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ addOneshotsToCollection() {
+ this.$store.dispatch('dialogAddSeriesToCollection', this.selectedBooks.map(b => b.seriesId))
+ this.selectedBooks = []
},
- editMultipleBooks() {
- this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
+ async editMultipleSeries() {
+ 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)
+ } else
+ this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ },
+ 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)
},
deleteSeries() {
this.$store.dispatch('dialogDeleteSeries', this.selectedSeries)
diff --git a/komga-webui/src/views/DuplicateFiles.vue b/komga-webui/src/views/DuplicateFiles.vue
index 1ae71cf7..b786215b 100644
--- a/komga-webui/src/views/DuplicateFiles.vue
+++ b/komga-webui/src/views/DuplicateFiles.vue
@@ -16,7 +16,10 @@
}"
>
- {{ item.url }}
+
+ {{ item.url }}
+
diff --git a/komga-webui/src/views/ImportBooks.vue b/komga-webui/src/views/ImportBooks.vue
index dd554bdf..1a60fa90 100644
--- a/komga-webui/src/views/ImportBooks.vue
+++ b/komga-webui/src/views/ImportBooks.vue
@@ -82,7 +82,7 @@
{{ $t('book_import.button_select_series') }}
-
+
diff --git a/komga-webui/src/views/MediaAnalysis.vue b/komga-webui/src/views/MediaAnalysis.vue
index 7b312157..26328260 100644
--- a/komga-webui/src/views/MediaAnalysis.vue
+++ b/komga-webui/src/views/MediaAnalysis.vue
@@ -14,7 +14,10 @@
}"
>
- {{ item.name }}
+
+ {{ item.name }}
+
diff --git a/komga-webui/src/views/SearchView.vue b/komga-webui/src/views/SearchView.vue
index 89616f07..98bcf6e1 100644
--- a/komga-webui/src/views/SearchView.vue
+++ b/komga-webui/src/views/SearchView.vue
@@ -20,10 +20,12 @@
b.oneshot)
+ },
},
methods: {
async scrollChanged(loader: PageLoader, percent: number) {
@@ -299,11 +304,19 @@ export default Vue.extend({
this.reloadResults()
}
},
- singleEditSeries(series: SeriesDto) {
- this.$store.dispatch('dialogUpdateSeries', series)
+ 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('dialogUpdateSeries', series)
},
- singleEditBook(book: BookDto) {
- this.$store.dispatch('dialogUpdateBooks', book)
+ 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)
},
singleEditCollection(collection: CollectionDto) {
this.$store.dispatch('dialogEditCollection', collection)
@@ -333,11 +346,24 @@ export default Vue.extend({
addToReadList() {
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks.map(b => b.id))
},
- editMultipleSeries() {
- this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ addOneshotsToCollection() {
+ this.$store.dispatch('dialogAddSeriesToCollection', this.selectedBooks.map(b => b.seriesId))
},
- editMultipleBooks() {
- this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
+ async editMultipleSeries() {
+ 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)
+ } else
+ this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
+ },
+ 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)
},
bulkEditMultipleBooks() {
this.$store.dispatch('dialogUpdateBulkBooks', this.selectedBooks)