mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
feat(webui): oneshots handling
This commit is contained in:
parent
39e7ae9e64
commit
2b238cccaf
30 changed files with 2414 additions and 112 deletions
|
|
@ -31,7 +31,7 @@
|
|||
<template v-else>
|
||||
<div style="height: 2em" class="missing"></div>
|
||||
</template>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries" :include-oneshots="false"/>
|
||||
</td>
|
||||
|
||||
<!-- Book number chooser -->
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<div class="unread" v-if="isUnread"/>
|
||||
|
||||
<!-- unread count for series -->
|
||||
<span v-if="unreadCount"
|
||||
<span v-else-if="unreadCount"
|
||||
class="white--text pa-1 px-2 text-subtitle-2"
|
||||
:style="{background: 'orange', position: 'absolute', right: 0}"
|
||||
>
|
||||
|
|
@ -72,11 +72,19 @@
|
|||
<div v-if="!selected && !preselect && actionMenu"
|
||||
:style="'position: absolute; bottom: 5px; ' + ($vuetify.rtl ? 'left' : 'right') +': 5px'"
|
||||
>
|
||||
<book-actions-menu v-if="computedItem.type() === ItemTypes.BOOK"
|
||||
<one-shot-actions-menu v-if="computedItem.type() === ItemTypes.BOOK && item.oneshot"
|
||||
:book="item"
|
||||
:menu.sync="actionMenuState"
|
||||
/>
|
||||
<book-actions-menu v-if="computedItem.type() === ItemTypes.BOOK && !item.oneshot"
|
||||
:book="item"
|
||||
:menu.sync="actionMenuState"
|
||||
/>
|
||||
<series-actions-menu v-if="computedItem.type() === ItemTypes.SERIES"
|
||||
<one-shot-actions-menu v-if="computedItem.type() === ItemTypes.SERIES && item.oneshot"
|
||||
:series="item"
|
||||
:menu.sync="actionMenuState"
|
||||
/>
|
||||
<series-actions-menu v-if="computedItem.type() === ItemTypes.SERIES && !item.oneshot"
|
||||
:series="item"
|
||||
:menu.sync="actionMenuState"
|
||||
/>
|
||||
|
|
@ -128,7 +136,7 @@
|
|||
</router-link>
|
||||
</v-card-subtitle>
|
||||
</template>
|
||||
<v-card-text class="px-2 font-weight-light" v-html="body">
|
||||
<v-card-text class="px-2 pt-0 font-weight-light" v-html="body">
|
||||
</v-card-text>
|
||||
</template>
|
||||
</v-card>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@
|
|||
:books="updateBulkBooks"
|
||||
/>
|
||||
|
||||
<edit-oneshot-dialog
|
||||
v-model="updateOneshotsDialog"
|
||||
:oneshots="updateOneshots"
|
||||
/>
|
||||
|
||||
<edit-series-dialog
|
||||
v-model="updateSeriesDialog"
|
||||
:series="updateSeries"
|
||||
|
|
@ -103,16 +108,18 @@ import Vue from 'vue'
|
|||
import ReadListAddToDialog from '@/components/dialogs/ReadListAddToDialog.vue'
|
||||
import ReadListEditDialog from '@/components/dialogs/ReadListEditDialog.vue'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import {ERROR} from '@/types/events'
|
||||
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import BulkEditBooksDialog from '@/components/dialogs/BulkEditBooksDialog.vue'
|
||||
import {ReadListDto} from '@/types/komga-readlists'
|
||||
import EditOneshotDialog from '@/components/dialogs/EditOneshotDialog.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ReusableDialogs',
|
||||
components: {
|
||||
EditOneshotDialog,
|
||||
BulkEditBooksDialog,
|
||||
ConfirmationDialog,
|
||||
CollectionAddToDialog,
|
||||
|
|
@ -259,6 +266,18 @@ export default Vue.extend({
|
|||
booksToDeleteSingle(): boolean {
|
||||
return !Array.isArray(this.booksToDelete)
|
||||
},
|
||||
// oneshots
|
||||
updateOneshotsDialog: {
|
||||
get(): boolean {
|
||||
return this.$store.state.updateOneshotsDialog
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('dialogUpdateOneshotsDisplay', val)
|
||||
},
|
||||
},
|
||||
updateOneshots(): Oneshot | Oneshot[] {
|
||||
return this.$store.state.updateOneshots
|
||||
},
|
||||
// series
|
||||
updateSeriesDialog: {
|
||||
get(): boolean {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,10 @@
|
|||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ data.item.metadata.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ data.item.seriesTitle }} - {{ data.item.metadata.number }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="!data.item.oneshot">{{ data.item.seriesTitle }} - {{
|
||||
data.item.metadata.number
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>{{
|
||||
$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',
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="addToCollection" v-if="isAdmin && kind === 'series'">
|
||||
<v-btn icon @click="addToCollection" v-if="isAdmin && (kind === 'series' || (kind === 'books' && oneshots))">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-playlist-plus</v-icon>
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="addToReadList" v-if="isAdmin && kind === 'books'">
|
||||
<v-btn icon @click="addToReadList" v-if="isAdmin && (kind === 'books' || (kind === 'series' && oneshots))">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-book-plus-multiple</v-icon>
|
||||
|
|
@ -114,6 +114,10 @@ export default Vue.extend({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
oneshots: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showSelectAll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
|
|||
1128
komga-webui/src/components/dialogs/EditOneshotDialog.vue
Normal file
1128
komga-webui/src/components/dialogs/EditOneshotDialog.vue
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -108,6 +108,22 @@
|
|||
</v-tooltip>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<v-text-field v-model="form.oneshotsDirectory"
|
||||
clearable
|
||||
:label="$t('dialog.edit_library.field_oneshotsdirectory')"
|
||||
:error-messages="getErrors('oneshotsDirectory')"
|
||||
class="mx-4 mt-4"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on" color="info">mdi-help-circle-outline</v-icon>
|
||||
</template>
|
||||
{{ $t('dialog.edit_library.tooltip_oneshotsdirectory') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ export default Vue.extend({
|
|||
type: Object as PropType<SeriesDto>,
|
||||
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()
|
||||
|
|
|
|||
111
komga-webui/src/components/menus/OneshotActionsMenu.vue
Normal file
111
komga-webui/src/components/menus/OneshotActionsMenu.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-menu offset-y v-model="menuState">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on" @click.prevent="">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="analyze" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.analyze') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="refreshMetadata" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.refresh_metadata') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToCollection" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.add_to_collection') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToReadList" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.add_to_readlist') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="markRead" v-if="!isRead">
|
||||
<v-list-item-title>{{ $t('menu.mark_read') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="promptDelete" class="list-danger" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {getReadProgress} from '@/functions/book-progress'
|
||||
import {ReadStatus} from '@/types/enum-books'
|
||||
import Vue from 'vue'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'OneShotActionsMenu',
|
||||
data: () => {
|
||||
return {
|
||||
menuState: false,
|
||||
localBookId: undefined as unknown as string,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
book: {
|
||||
type: Object as () => BookDto,
|
||||
required: false,
|
||||
},
|
||||
series: {
|
||||
type: Object as () => SeriesDto,
|
||||
required: false,
|
||||
},
|
||||
menu: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
menuState(val) {
|
||||
this.$emit('update:menu', val)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isAdmin(): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
isRead(): boolean {
|
||||
return this.series ? this.series.booksReadCount === this.series.booksCount : getReadProgress(this.book) === ReadStatus.READ
|
||||
},
|
||||
isUnread(): boolean {
|
||||
return this.series ? this.series.booksUnreadCount === this.series.booksCount : getReadProgress(this.book) === ReadStatus.UNREAD
|
||||
},
|
||||
seriesId(): string {
|
||||
return this.series ? this.series.id : this.book?.seriesId
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
analyze() {
|
||||
if (this.book) this.$komgaBooks.analyzeBook(this.book)
|
||||
else this.$komgaSeries.analyzeSeries(this.series)
|
||||
},
|
||||
refreshMetadata() {
|
||||
if (this.book) this.$komgaBooks.refreshMetadata(this.book)
|
||||
this.$komgaSeries.refreshMetadata(this.series)
|
||||
},
|
||||
addToCollection() {
|
||||
this.$store.dispatch('dialogAddSeriesToCollection', [this.seriesId])
|
||||
},
|
||||
async addToReadList() {
|
||||
if (!this.book && !this.localBookId) this.localBookId = (await this.$komgaSeries.getBooks(this.seriesId)).content[0].id
|
||||
this.$store.dispatch('dialogAddBooksToReadList', [this.book?.id || this.localBookId])
|
||||
},
|
||||
async markRead() {
|
||||
await this.$komgaSeries.markAsRead(this.seriesId)
|
||||
},
|
||||
async markUnread() {
|
||||
await this.$komgaSeries.markAsUnread(this.seriesId)
|
||||
},
|
||||
promptDelete() {
|
||||
if (this.book) this.$store.dispatch('dialogDeleteBook', this.book)
|
||||
else this.$store.dispatch('dialogDeleteSeries', this.series)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<Page<SeriesDto>> {
|
||||
searchRegex?: string, complete?: boolean, sharingLabel?: string[], oneshot?: boolean): Promise<Page<SeriesDto>> {
|
||||
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<GroupCountDto[]> {
|
||||
complete?: boolean, sharingLabel?: string[], oneshot?: boolean): Promise<GroupCountDto[]> {
|
||||
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<Page<SeriesDto>> {
|
||||
async getNewSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
|
||||
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<Page<SeriesDto>> {
|
||||
async getUpdatedSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ export class BookItem extends Item<BookDto> {
|
|||
|
||||
|
||||
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<BookDto> {
|
|||
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<BookDto> {
|
|||
text = getFileSize(this.item.sizeBytes)
|
||||
else
|
||||
text = i18n.tc('common.pages_n', this.item.media.pagesCount)
|
||||
return `<div class="text-truncate" title="${title}">${text}</div>`
|
||||
return `<div class="text-truncate" title="${title ? title : ''}">${text}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
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<SeriesDto> {
|
|||
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 `<div class="text-truncate" title="${title}">${text}</div>`
|
||||
return `<div class="text-truncate" title="${title ? title : ''}">${text}</div>`
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface BookDto {
|
|||
metadata: BookMetadataDto,
|
||||
readProgress?: ReadProgressDto,
|
||||
deleted: boolean,
|
||||
oneshot: boolean,
|
||||
|
||||
// custom fields
|
||||
context: Context
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
<v-list-item @click="setCurrentPageAsPoster(ItemTypes.SERIES)">
|
||||
<v-list-item-title>{{ $t('bookreader.set_current_page_as_series_poster') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="contextReadList" @click="setCurrentPageAsPoster(ItemTypes.READLIST)">
|
||||
<v-list-item v-if="contextReadList" @click="setCurrentPageAsPoster(ItemTypes.READLIST)">
|
||||
<v-list-item-title>{{ $t('bookreader.set_current_page_as_readlist_poster') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<v-btn
|
||||
icon
|
||||
:disabled="$_.isEmpty(siblingPrevious)"
|
||||
:to="{ name: 'browse-book', params: { bookId: previousId }, query: { context: context.origin, contextId: context.id} }"
|
||||
:to="{ name: siblingPrevious.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: siblingPrevious.seriesId, bookId: previousId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
|
|
@ -64,12 +64,13 @@
|
|||
<v-list-item
|
||||
v-for="(book, i) in siblings"
|
||||
:key="i"
|
||||
:to="{ name: 'browse-book', params: { bookId: book.id }, query: { context: context.origin, contextId: context.id} }"
|
||||
:to="{ name: book.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: book.seriesId, bookId: book.id }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<v-list-item-title class="text-wrap text-body-2">
|
||||
<template v-if="contextReadList">{{ book.seriesTitle }} {{ book.metadata.number }}:
|
||||
<template v-if="contextReadList && !book.oneshot">{{ book.seriesTitle }} {{ book.metadata.number }}:
|
||||
{{ book.metadata.title }}
|
||||
</template>
|
||||
<template v-else-if="contextReadList && book.oneshot">{{ book.metadata.title }}</template>
|
||||
<template v-else>{{ book.metadata.number }} - {{ book.metadata.title }}</template>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
<v-btn
|
||||
icon
|
||||
:disabled="$_.isEmpty(siblingNext)"
|
||||
:to="{ name: 'browse-book', params: { bookId: nextId }, query: { context: context.origin, contextId: context.id} }"
|
||||
:to="{ name: siblingNext.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: siblingNext.seriesId, bookId: nextId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
|
|
@ -159,7 +160,10 @@
|
|||
|
||||
<v-col cols="auto" v-if="book.metadata.releaseDate">
|
||||
{{
|
||||
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))
|
||||
}}
|
||||
</v-col>
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@
|
|||
<multi-select-bar
|
||||
v-model="selectedSeries"
|
||||
kind="series"
|
||||
:oneshots="selectedOneshots"
|
||||
show-select-all
|
||||
@unselect-all="selectedSeries = []"
|
||||
@select-all="selectedSeries = series"
|
||||
@mark-read="markSelectedRead"
|
||||
@mark-unread="markSelectedUnread"
|
||||
@add-to-collection="addToCollection"
|
||||
@add-to-readlist="addToReadList"
|
||||
@edit="editMultipleSeries"
|
||||
@delete="deleteSeries"
|
||||
/>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@
|
|||
<multi-select-bar
|
||||
v-model="selectedSeries"
|
||||
kind="series"
|
||||
:oneshots="selectedOneshots"
|
||||
show-select-all
|
||||
@unselect-all="selectedSeries = []"
|
||||
@select-all="selectedSeries = series"
|
||||
@mark-read="markSelectedRead"
|
||||
@mark-unread="markSelectedUnread"
|
||||
@add-to-collection="addToCollection"
|
||||
@add-to-readlist="addToReadList"
|
||||
@edit="editMultipleSeries"
|
||||
@delete="deleteSeries"
|
||||
/>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
796
komga-webui/src/views/BrowseOneshot.vue
Normal file
796
komga-webui/src/views/BrowseOneshot.vue
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
<template>
|
||||
<div v-if="!$_.isEmpty(book) && !$_.isEmpty(series)">
|
||||
<toolbar-sticky>
|
||||
<v-tooltip bottom :disabled="!isAdmin">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon
|
||||
v-on="on"
|
||||
:to="parentLocation"
|
||||
>
|
||||
<rtl-icon icon="mdi-arrow-left" rtl="mdi-arrow-right"/>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span v-if="context.origin === ContextOrigin.READLIST">{{ $t('common.go_to_readlist') }}</span>
|
||||
<span v-else-if="context.origin === ContextOrigin.COLLECTION">{{ $t('common.go_to_collection') }}</span>
|
||||
<span v-else>{{ $t('common.go_to_library') }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
<!-- Action menu -->
|
||||
<oneshot-actions-menu v-if="book"
|
||||
:book="book"
|
||||
:series="series"
|
||||
/>
|
||||
|
||||
<v-btn icon @click="editBook" v-if="isAdmin">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<!-- Context notification for navigation -->
|
||||
<v-alert
|
||||
v-if="context.origin === ContextOrigin.READLIST && $vuetify.breakpoint.mdAndUp"
|
||||
type="info"
|
||||
text
|
||||
dense
|
||||
border="right"
|
||||
class="mb-0"
|
||||
>{{ $t('browse_book.navigation_within_readlist', {name: contextName}) }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Navigate to previous book -->
|
||||
<v-btn
|
||||
v-if="context.origin === ContextOrigin.READLIST"
|
||||
icon
|
||||
:disabled="$_.isEmpty(siblingPrevious)"
|
||||
:to="{ name: siblingPrevious.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: siblingPrevious.seriesId, bookId: previousId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
|
||||
<!-- List of all books in context (series/readlist) for navigation -->
|
||||
<v-menu
|
||||
v-if="context.origin === ContextOrigin.READLIST"
|
||||
bottom
|
||||
offset-y
|
||||
:max-height="$vuetify.breakpoint.height * .7"
|
||||
:max-width="250"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list
|
||||
flat
|
||||
>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item
|
||||
v-for="(book, i) in siblings"
|
||||
:key="i"
|
||||
:to="{ name: book.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: book.seriesId, bookId: book.id }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<v-list-item-title class="text-wrap text-body-2">
|
||||
<template v-if="context.origin === ContextOrigin.READLIST && !book.oneshot">{{ book.seriesTitle }}
|
||||
{{ book.metadata.number }}:
|
||||
{{ book.metadata.title }}
|
||||
</template>
|
||||
<template v-else-if="context.origin === ContextOrigin.READLIST && book.oneshot">{{
|
||||
book.metadata.title
|
||||
}}
|
||||
</template>
|
||||
<template v-else>{{ book.metadata.number }} - {{ book.metadata.title }}</template>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<!-- Navigate to next book -->
|
||||
<v-btn
|
||||
v-if="context.origin === ContextOrigin.READLIST"
|
||||
icon
|
||||
:disabled="$_.isEmpty(siblingNext)"
|
||||
:to="{ name: siblingNext.oneshot ? 'browse-oneshot' : 'browse-book', params: { seriesId: siblingNext.seriesId, bookId: nextId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
</toolbar-sticky>
|
||||
|
||||
<v-container fluid class="pa-6">
|
||||
<!-- Context notification for navigation -->
|
||||
<v-row>
|
||||
<v-alert
|
||||
v-if="context.origin === ContextOrigin.READLIST && $vuetify.breakpoint.smAndDown"
|
||||
type="info"
|
||||
text
|
||||
dense
|
||||
border="right"
|
||||
class="mb-0"
|
||||
>{{ $t('browse_book.navigation_within_readlist', {name: contextName}) }}
|
||||
</v-alert>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="4" sm="4" md="auto" lg="auto" xl="auto">
|
||||
<item-card
|
||||
v-if="book.hasOwnProperty('id')"
|
||||
width="212"
|
||||
:item="book"
|
||||
thumbnail-only
|
||||
no-link
|
||||
:action-menu="false"
|
||||
></item-card>
|
||||
<div v-if="isInProgress"
|
||||
class="text-caption text-center mt-1"
|
||||
:title="$t('common.read_on', {date: readProgressDate})"
|
||||
>
|
||||
{{ $tc('common.pages_left', pagesLeft) }}
|
||||
</div>
|
||||
<div v-else-if="isRead"
|
||||
class="text-caption text-center mt-1"
|
||||
>
|
||||
{{ $t('common.read_on', {date: readProgressDate}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="8">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col class="py-1">
|
||||
<span class="text-h6">{{ book.metadata.title }}</span>
|
||||
<router-link
|
||||
class="caption link-underline"
|
||||
:class="$vuetify.breakpoint.smAndUp ? 'mx-1' : ''"
|
||||
:style="$vuetify.breakpoint.xsOnly ? 'display: block' : ''"
|
||||
:to="{name:'browse-libraries', params: {libraryId: book.libraryId }}"
|
||||
>{{ $t('searchbox.in_library', {library: getLibraryName(book)}) }}
|
||||
</router-link>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="text-body-2">
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata && series.metadata.ageRating">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {ageRating: [series.metadata.ageRating]}}"
|
||||
>
|
||||
{{ series.metadata.ageRating }}+
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata.language">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {language: [series.metadata.language]}}"
|
||||
>
|
||||
{{ languageDisplay }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto"
|
||||
v-if="series.metadata.readingDirection">
|
||||
<v-chip label small>
|
||||
{{ $t(`enums.reading_direction.${series.metadata.readingDirection}`) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="unavailable">
|
||||
<v-chip label small color="error">
|
||||
{{ $t('common.unavailable') }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="text-caption" align="center">
|
||||
<v-col cols="auto" v-if="book.media.status === MediaStatus.UNKNOWN">
|
||||
{{ $t('book_card.unknown') }}
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto" v-else>
|
||||
{{ $tc('common.pages_n', book.media.pagesCount) }}
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto" v-if="book.metadata.releaseDate">
|
||||
{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
dateStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(book.metadata.releaseDate))
|
||||
}}
|
||||
</v-col>
|
||||
|
||||
<v-col class="py-1 pe-0"
|
||||
cols="auto"
|
||||
v-if="book.media.status === MediaStatus.OUTDATED">
|
||||
<v-tooltip bottom :disabled="!isAdmin">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-chip label small color="warning" v-on="on">
|
||||
{{ $t('common.outdated') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
{{ $t('browse_book.outdated_tooltip') }}
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="unavailable">
|
||||
<v-chip label small color="error">
|
||||
{{ $t('common.unavailable') }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-if="$vuetify.breakpoint.smAndUp">
|
||||
<v-row class="align-center">
|
||||
<v-col cols="auto">
|
||||
<v-btn color="accent"
|
||||
small
|
||||
:title="$t('browse_book.read_book')"
|
||||
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
|
||||
:disabled="!canRead"
|
||||
>
|
||||
<v-icon left small>mdi-book-open-page-variant</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn small
|
||||
:title="$t('browse_book.read_incognito')"
|
||||
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
|
||||
:disabled="!canRead"
|
||||
>
|
||||
<v-icon left small>mdi-incognito</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :title="$t('browse_book.download_file')"
|
||||
small
|
||||
:disabled="!canDownload"
|
||||
:href="fileUrl">
|
||||
<v-icon left small>mdi-file-download</v-icon>
|
||||
{{ $t('common.download') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.summary">
|
||||
<v-col>
|
||||
<read-more>{{ book.metadata.summary }}</read-more>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-if="$vuetify.breakpoint.xsOnly">
|
||||
<v-row class="align-center">
|
||||
<v-col cols="auto">
|
||||
<v-btn color="accent"
|
||||
small
|
||||
:title="$t('browse_book.read_book')"
|
||||
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id}}"
|
||||
:disabled="!canRead"
|
||||
>
|
||||
<v-icon left small>mdi-book-open-page-variant</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn small
|
||||
:title="$t('browse_book.read_incognito')"
|
||||
:to="{name: 'read-book', params: { bookId: book.id}, query: { context: context.origin, contextId: context.id, incognito: true}}"
|
||||
:disabled="!canRead"
|
||||
>
|
||||
<v-icon left small>mdi-incognito</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :title="$t('browse_book.download_file')"
|
||||
small
|
||||
:disabled="!canDownload"
|
||||
:href="fileUrl">
|
||||
<v-icon left small>mdi-file-download</v-icon>
|
||||
{{ $t('common.download') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.summary">
|
||||
<v-col>
|
||||
<read-more>{{ book.metadata.summary }}</read-more>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<!-- Publisher -->
|
||||
<v-row v-if="series.metadata.publisher" class="align-center text-caption">
|
||||
<v-col cols="4" sm="3" md="2" xl="1" class="py-1 text-uppercase">{{ $t('common.publisher') }}</v-col>
|
||||
<v-col cols="8" sm="9" md="10" xl="11" class="py-1">
|
||||
<v-chip
|
||||
class="me-2"
|
||||
:title="series.metadata.publisher"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {publisher: [series.metadata.publisher]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
link
|
||||
>{{ series.metadata.publisher }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Genres -->
|
||||
<v-row v-if="series.metadata.genres.length > 0" class="align-center text-caption">
|
||||
<v-col cols="4" sm="3" md="2" xl="1" class="py-1 text-uppercase">{{ $t('common.genre') }}</v-col>
|
||||
<v-col cols="8" sm="9" md="10" xl="11" class="py-1 text-capitalize">
|
||||
<vue-horizontal>
|
||||
<template v-slot:btn-prev>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:btn-next>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-chip v-for="(t, i) in $_.sortBy(series.metadata.genres)"
|
||||
:key="i"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {genre: [t]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
link
|
||||
>{{ t }}
|
||||
</v-chip>
|
||||
</vue-horizontal>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-for="role in displayedRoles"
|
||||
:key="role"
|
||||
class="align-center text-caption"
|
||||
>
|
||||
<v-col cols="4" sm="3" md="2" xl="1" class="py-1 text-uppercase">
|
||||
{{ $te(`author_roles.${role}`) ? $t(`author_roles.${role}`) : role }}
|
||||
</v-col>
|
||||
<v-col cols="8" sm="9" md="10" xl="11" class="py-1">
|
||||
<vue-horizontal>
|
||||
<template v-slot:btn-prev>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:btn-next>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-chip v-for="(name, i) in authorsByRole[role]"
|
||||
:key="i"
|
||||
class="me-2"
|
||||
:title="name"
|
||||
:to="{name:'browse-libraries', params: {libraryId: book.libraryId }, query: {[role]: [name]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
link
|
||||
>{{ name }}
|
||||
</v-chip>
|
||||
</vue-horizontal>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.tags.length > 0" class="align-center text-caption">
|
||||
<v-col cols="4" sm="3" md="2" xl="1" class="py-1 text-uppercase">{{ $i18n.t('common.tags') }}</v-col>
|
||||
<v-col cols="8" sm="9" md="10" xl="11" class="py-1 text-capitalize">
|
||||
<vue-horizontal>
|
||||
<template v-slot:btn-prev>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:btn-next>
|
||||
<v-btn icon small>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-chip v-for="(t, i) in book.metadata.tags"
|
||||
:key="i"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: book.libraryId}, query: {tag: [t]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
link
|
||||
>{{ t }}
|
||||
</v-chip>
|
||||
</vue-horizontal>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-1">
|
||||
<collections-expansion-panels :collections="collections"/>
|
||||
</v-col>
|
||||
<v-col cols="12" class="pt-1">
|
||||
<read-lists-expansion-panels :read-lists="readLists"/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.links.length > 0" class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.links') }}</v-col>
|
||||
<v-col class="py-1" cols="8" sm="9" md="10" xl="11">
|
||||
<v-chip
|
||||
v-for="(link, i) in book.metadata.links"
|
||||
:href="link.url"
|
||||
target="_blank"
|
||||
class="me-2"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
link
|
||||
:key="i"
|
||||
>
|
||||
{{ link.label }}
|
||||
<v-icon
|
||||
x-small
|
||||
color="grey"
|
||||
class="ps-1"
|
||||
>
|
||||
mdi-open-in-new
|
||||
</v-icon>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.size') }}</v-col>
|
||||
<v-col class="py-1" cols="8" sm="9" md="10" xl="11">{{ book.size }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.media.comment" class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.comment') }}</v-col>
|
||||
<v-col class="py-1 error--text font-weight-bold" cols="8" sm="9" md="10" xl="11">{{ mediaComment }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.format') }}</v-col>
|
||||
<v-col class="py-1" cols="8" sm="9" md="10" xl="11">{{ format.type }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.isbn" class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.isbn') }}</v-col>
|
||||
<v-col class="py-1" cols="8" sm="9" md="10" xl="11">{{ book.metadata.isbn }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="align-center text-caption">
|
||||
<v-col class="py-1 text-uppercase" cols="4" sm="3" md="2" xl="1">{{ $t('browse_book.file') }}</v-col>
|
||||
<v-col class="py-1" cols="8" sm="9" md="10" xl="11">{{ book.url }}</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
|
||||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
|
||||
import {groupAuthorsByRole} from '@/functions/authors'
|
||||
import {getBookFormatFromMediaType} from '@/functions/book-format'
|
||||
import {getPagesLeft, getReadProgress, getReadProgressPercentage} from '@/functions/book-progress'
|
||||
import {getBookTitleCompact} from '@/functions/book-title'
|
||||
import {bookFileUrl, seriesThumbnailUrl} from '@/functions/urls'
|
||||
import {MediaStatus, ReadStatus} from '@/types/enum-books'
|
||||
import {
|
||||
BOOK_CHANGED,
|
||||
BOOK_DELETED,
|
||||
COLLECTION_ADDED,
|
||||
COLLECTION_CHANGED,
|
||||
COLLECTION_DELETED,
|
||||
LIBRARY_DELETED,
|
||||
READLIST_ADDED,
|
||||
READLIST_CHANGED,
|
||||
READLIST_DELETED,
|
||||
READPROGRESS_CHANGED,
|
||||
READPROGRESS_DELETED,
|
||||
SERIES_CHANGED,
|
||||
SERIES_DELETED,
|
||||
} from '@/types/events'
|
||||
import Vue from 'vue'
|
||||
import ReadListsExpansionPanels from '@/components/ReadListsExpansionPanels.vue'
|
||||
import {BookDto, BookFormat} from '@/types/komga-books'
|
||||
import {Context, ContextOrigin} from '@/types/context'
|
||||
import ReadMore from '@/components/ReadMore.vue'
|
||||
import VueHorizontal from 'vue-horizontal'
|
||||
import {authorRoles} from '@/types/author-roles'
|
||||
import {convertErrorCodes} from '@/functions/error-codes'
|
||||
import RtlIcon from '@/components/RtlIcon.vue'
|
||||
import {
|
||||
BookSseDto,
|
||||
CollectionSseDto,
|
||||
LibrarySseDto,
|
||||
ReadListSseDto,
|
||||
ReadProgressSseDto,
|
||||
SeriesSseDto,
|
||||
} from '@/types/komga-sse'
|
||||
import {RawLocation} from 'vue-router/types/router'
|
||||
import {ReadListDto} from '@/types/komga-readlists'
|
||||
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import CollectionsExpansionPanels from '@/components/CollectionsExpansionPanels.vue'
|
||||
import OneshotActionsMenu from '@/components/menus/OneshotActionsMenu.vue'
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseOneshot',
|
||||
components: {
|
||||
OneshotActionsMenu,
|
||||
CollectionsExpansionPanels,
|
||||
ReadMore, ToolbarSticky, ItemCard, ReadListsExpansionPanels, VueHorizontal, RtlIcon,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
MediaStatus,
|
||||
ContextOrigin,
|
||||
book: {} as BookDto,
|
||||
series: {} as SeriesDto,
|
||||
context: {} as Context,
|
||||
siblings: [] as BookDto[],
|
||||
siblingPrevious: {} as BookDto,
|
||||
siblingNext: {} as BookDto,
|
||||
contextName: '',
|
||||
collections: [] as CollectionDto[],
|
||||
readLists: [] as ReadListDto[],
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.loadSeries(this.seriesId)
|
||||
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
|
||||
this.$eventHub.$on(SERIES_DELETED, this.seriesDeleted)
|
||||
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
|
||||
this.$eventHub.$on(BOOK_DELETED, this.bookDeleted)
|
||||
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
|
||||
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
|
||||
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
|
||||
this.$eventHub.$on(READLIST_ADDED, this.readListChanged)
|
||||
this.$eventHub.$on(READLIST_CHANGED, this.readListChanged)
|
||||
this.$eventHub.$on(READLIST_DELETED, this.readListChanged)
|
||||
this.$eventHub.$on(COLLECTION_ADDED, this.collectionChanged)
|
||||
this.$eventHub.$on(COLLECTION_CHANGED, this.collectionChanged)
|
||||
this.$eventHub.$on(COLLECTION_DELETED, this.collectionChanged)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
|
||||
this.$eventHub.$off(SERIES_DELETED, this.seriesDeleted)
|
||||
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
|
||||
this.$eventHub.$off(BOOK_DELETED, this.bookDeleted)
|
||||
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
|
||||
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
|
||||
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
|
||||
this.$eventHub.$off(READLIST_ADDED, this.readListChanged)
|
||||
this.$eventHub.$off(READLIST_CHANGED, this.readListChanged)
|
||||
this.$eventHub.$off(READLIST_DELETED, this.readListChanged)
|
||||
this.$eventHub.$off(COLLECTION_ADDED, this.collectionChanged)
|
||||
this.$eventHub.$off(COLLECTION_CHANGED, this.collectionChanged)
|
||||
this.$eventHub.$off(COLLECTION_DELETED, this.collectionChanged)
|
||||
},
|
||||
props: {
|
||||
seriesId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
async beforeRouteUpdate(to, from, next) {
|
||||
if (to.params.seriesId !== from.params.seriesId) {
|
||||
this.loadSeries(to.params.seriesId)
|
||||
}
|
||||
|
||||
next()
|
||||
},
|
||||
computed: {
|
||||
isAdmin(): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
unavailable(): boolean {
|
||||
return this.book.deleted || this.$store.getters.getLibraryById(this.book.libraryId).unavailable
|
||||
},
|
||||
canRead(): boolean {
|
||||
return this.book.media.status === 'READY' && this.$store.getters.mePageStreaming && !this.unavailable
|
||||
},
|
||||
canDownload(): boolean {
|
||||
return this.$store.getters.meFileDownload && !this.unavailable
|
||||
},
|
||||
thumbnailUrl(): string {
|
||||
return seriesThumbnailUrl(this.seriesId)
|
||||
},
|
||||
fileUrl(): string {
|
||||
return bookFileUrl(this.book.id)
|
||||
},
|
||||
format(): BookFormat {
|
||||
return getBookFormatFromMediaType(this.book.media.mediaType)
|
||||
},
|
||||
authorsByRole(): any {
|
||||
return groupAuthorsByRole(this.book.metadata.authors)
|
||||
},
|
||||
isRead(): boolean {
|
||||
return getReadProgress(this.book) === ReadStatus.READ
|
||||
},
|
||||
isUnread(): boolean {
|
||||
return getReadProgress(this.book) === ReadStatus.UNREAD
|
||||
},
|
||||
isInProgress(): boolean {
|
||||
return getReadProgress(this.book) === ReadStatus.IN_PROGRESS
|
||||
},
|
||||
pagesLeft(): number {
|
||||
return getPagesLeft(this.book)
|
||||
},
|
||||
readProgressPercentage(): number {
|
||||
return getReadProgressPercentage(this.book)
|
||||
},
|
||||
readProgressDate(): string | undefined {
|
||||
if (this.book.readProgress)
|
||||
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
} as Intl.DateTimeFormatOptions).format(this.book.readProgress.readDate)
|
||||
return undefined
|
||||
},
|
||||
previousId(): string {
|
||||
return this.siblingPrevious?.id?.toString() || '0'
|
||||
},
|
||||
nextId(): string {
|
||||
return this.siblingNext?.id?.toString() || '0'
|
||||
},
|
||||
mediaComment(): string {
|
||||
return convertErrorCodes(this.book.media.comment)
|
||||
},
|
||||
parentLocation(): RawLocation {
|
||||
if (this.context.origin === ContextOrigin.READLIST)
|
||||
return {name: 'browse-readlist', params: {readListId: this.context.id}}
|
||||
else if (this.context.origin === ContextOrigin.COLLECTION)
|
||||
return {name: 'browse-collection', params: {collectionId: this.context.id}}
|
||||
else
|
||||
return {name: 'browse-libraries', params: {libraryId: this.book.libraryId}}
|
||||
},
|
||||
displayedRoles(): string[] {
|
||||
const allRoles = this.$_.uniq([...authorRoles, ...(this.book.metadata.authors.map(x => x.role))])
|
||||
return allRoles.filter(x => this.authorsByRole[x])
|
||||
},
|
||||
languageDisplay(): string {
|
||||
return tags(this.series.metadata.language).language().descriptions()[0]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getLibraryName(item: BookDto): string {
|
||||
return this.$store.getters.getLibraryById(item.libraryId).name
|
||||
},
|
||||
libraryDeleted(event: LibrarySseDto) {
|
||||
if (event.libraryId === this.book.libraryId) {
|
||||
this.$router.push({name: 'home'})
|
||||
}
|
||||
},
|
||||
readListChanged(event: ReadListSseDto) {
|
||||
if (event.bookIds.includes(this.book.id) || this.readLists.map(x => x.id).includes(event.readListId)) {
|
||||
this.$komgaBooks.getReadLists(this.book.id)
|
||||
.then(v => this.readLists = v)
|
||||
}
|
||||
},
|
||||
collectionChanged(event: CollectionSseDto) {
|
||||
if (event.seriesIds.includes(this.seriesId) || this.collections.map(x => x.id).includes(event.collectionId)) {
|
||||
this.$komgaSeries.getCollections(this.seriesId)
|
||||
.then(v => this.collections = v)
|
||||
}
|
||||
},
|
||||
seriesChanged(event: SeriesSseDto) {
|
||||
if (event.seriesId === this.seriesId)
|
||||
this.$komgaSeries.getOneSeries(this.seriesId)
|
||||
.then(v => this.series = v)
|
||||
},
|
||||
seriesDeleted(event: SeriesSseDto) {
|
||||
if (event.seriesId === this.seriesId) {
|
||||
this.$router.push({name: 'browse-libraries', params: {libraryId: this.series.libraryId}})
|
||||
}
|
||||
},
|
||||
bookChanged(event: BookSseDto) {
|
||||
if (event.bookId === this.book.id) this.loadBook(this.seriesId)
|
||||
},
|
||||
bookDeleted(event: BookSseDto) {
|
||||
if (event.bookId === this.book.id) {
|
||||
this.$router.push({name: 'browse-libraries', params: {libraryId: this.book.libraryId}})
|
||||
}
|
||||
},
|
||||
readProgressChanged(event: ReadProgressSseDto) {
|
||||
if (event.bookId === this.book.id) this.loadBook(this.seriesId)
|
||||
},
|
||||
async loadSeries(seriesId: string) {
|
||||
this.$komgaSeries.getOneSeries(seriesId)
|
||||
.then(v => this.series = v)
|
||||
this.$komgaSeries.getCollections(seriesId)
|
||||
.then(v => this.collections = v)
|
||||
|
||||
// parse query params to get context and contextId
|
||||
if (this.$route.query.contextId && this.$route.query.context
|
||||
&& Object.values(ContextOrigin).includes(this.$route.query.context as ContextOrigin)) {
|
||||
this.context = {
|
||||
origin: this.$route.query.context as ContextOrigin,
|
||||
id: this.$route.query.contextId as string,
|
||||
}
|
||||
this.series.context = this.context
|
||||
}
|
||||
|
||||
await this.loadBook(seriesId)
|
||||
},
|
||||
async loadBook(seriesId: string) {
|
||||
this.book = (await this.$komgaSeries.getBooks(seriesId)).content[0]
|
||||
|
||||
// parse query params to get context and contextId
|
||||
if (this.$route.query.contextId && this.$route.query.context
|
||||
&& Object.values(ContextOrigin).includes(this.$route.query.context as ContextOrigin)) {
|
||||
this.context = {
|
||||
origin: this.$route.query.context as ContextOrigin,
|
||||
id: this.$route.query.contextId as string,
|
||||
}
|
||||
this.book.context = this.context
|
||||
if (this.context.origin === ContextOrigin.READLIST)
|
||||
this.$komgaReadLists.getOneReadList(this.context.id)
|
||||
.then(v => this.contextName = v.name)
|
||||
}
|
||||
|
||||
// Get siblings depending on origin
|
||||
if (this?.context.origin === ContextOrigin.READLIST) {
|
||||
this.$komgaReadLists.getBooks(this.context.id, {unpaged: true} as PageRequest)
|
||||
.then(v => this.siblings = v.content)
|
||||
} else {
|
||||
this.siblings = {} as BookDto[]
|
||||
}
|
||||
|
||||
this.$komgaBooks.getReadLists(this.book.id)
|
||||
.then(v => this.readLists = v)
|
||||
|
||||
if (this.$_.has(this.book, 'metadata.title')) {
|
||||
document.title = `Komga - ${getBookTitleCompact(this.book.metadata.title, this.book.seriesTitle)}`
|
||||
}
|
||||
|
||||
if (this?.context.origin === ContextOrigin.READLIST) {
|
||||
this.$komgaReadLists.getBookSiblingNext(this.context.id, this.book.id)
|
||||
.then(v => this.siblingNext = v)
|
||||
.catch(e => this.siblingNext = {} as BookDto)
|
||||
} else {
|
||||
this.siblingNext = {} as BookDto
|
||||
}
|
||||
if (this?.context.origin === ContextOrigin.READLIST) {
|
||||
this.$komgaReadLists.getBookSiblingPrevious(this.context.id, this.book.id)
|
||||
.then(v => this.siblingPrevious = v)
|
||||
.catch(e => this.siblingPrevious = {} as BookDto)
|
||||
} else {
|
||||
this.siblingPrevious = {} as BookDto
|
||||
}
|
||||
},
|
||||
analyze() {
|
||||
this.$komgaBooks.analyzeBook(this.book)
|
||||
},
|
||||
refreshMetadata() {
|
||||
this.$komgaBooks.refreshMetadata(this.book)
|
||||
},
|
||||
editBook() {
|
||||
this.$store.dispatch('dialogUpdateOneshots', {series: this.series, book: this.book} as Oneshot)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -47,11 +47,13 @@
|
|||
<multi-select-bar
|
||||
v-model="selectedBooks"
|
||||
kind="books"
|
||||
:oneshots="selectedOneshots"
|
||||
show-select-all
|
||||
@unselect-all="selectedBooks = []"
|
||||
@select-all="selectedBooks = books"
|
||||
@mark-read="markSelectedRead"
|
||||
@mark-unread="markSelectedUnread"
|
||||
@add-to-collection="addToCollection"
|
||||
@add-to-readlist="addToReadList"
|
||||
@edit="editMultipleBooks"
|
||||
@bulk-edit="bulkEditMultipleBooks"
|
||||
|
|
@ -186,6 +188,7 @@ import {ItemContext} from '@/types/items'
|
|||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import {ReadListDto, ReadListUpdateDto} from '@/types/komga-readlists'
|
||||
import {Oneshot} from '@/types/komga-series'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseReadList',
|
||||
|
|
@ -336,6 +339,9 @@ export default Vue.extend({
|
|||
filterActive(): boolean {
|
||||
return Object.keys(this.filters).some(x => 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))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@
|
|||
<multi-select-bar
|
||||
v-model="selectedBooks"
|
||||
kind="books"
|
||||
:oneshots="selectedOneshots"
|
||||
@unselect-all="selectedBooks = []"
|
||||
@mark-read="markSelectedBooksRead"
|
||||
@mark-unread="markSelectedBooksUnread"
|
||||
@add-to-readlist="addToReadList"
|
||||
@add-to-collection="addOneshotsToCollection"
|
||||
@edit="editMultipleBooks"
|
||||
@bulk-edit="bulkEditMultipleBooks"
|
||||
@delete="deleteBooks"
|
||||
|
|
@ -222,7 +224,7 @@ import {
|
|||
SERIES_DELETED,
|
||||
} from '@/types/events'
|
||||
import Vue from 'vue'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
|
||||
import {throttle} from 'lodash'
|
||||
import {subMonths} from 'date-fns'
|
||||
|
|
@ -327,6 +329,9 @@ export default Vue.extend({
|
|||
individualLibrary(): boolean {
|
||||
return this.libraryId !== LIBRARIES_ALL
|
||||
},
|
||||
selectedOneshots(): boolean {
|
||||
return this.selectedBooks.every(b => b.oneshot)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async scrollChanged(loader: PageLoader<any>, percent: number) {
|
||||
|
|
@ -383,11 +388,11 @@ export default Vue.extend({
|
|||
|
||||
this.loaderNewSeries = new PageLoader<SeriesDto>(
|
||||
{},
|
||||
(pageable: PageRequest) => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId), pageable),
|
||||
(pageable: PageRequest) => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId), false, pageable),
|
||||
)
|
||||
this.loaderUpdatedSeries = new PageLoader<SeriesDto>(
|
||||
{},
|
||||
(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)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
}"
|
||||
>
|
||||
<template v-slot:item.url="{ item }">
|
||||
<router-link :to="{name:'browse-book', params: {bookId: item.id}}">{{ item.url }}</router-link>
|
||||
<router-link
|
||||
:to="{name: item.oneshot ? 'browse-oneshot' : 'browse-book', params: {bookId: item.id, seriesId: item.seriesId}}">
|
||||
{{ item.url }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.deleted="{ item }">
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
<v-btn @click="modalSeriesPicker = true" :disabled="globalSelect === 0">
|
||||
{{ $t('book_import.button_select_series') }}
|
||||
</v-btn>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries" :include-oneshots="false"/>
|
||||
</v-col>
|
||||
<v-spacer/>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@
|
|||
}"
|
||||
>
|
||||
<template v-slot:item.name="{ item }">
|
||||
<router-link :to="{name:'browse-book', params: {bookId: item.id}}">{{ item.name }}</router-link>
|
||||
<router-link
|
||||
:to="{name: item.oneshot ? 'browse-oneshot' : 'browse-book', params: {bookId: item.id, seriesId: item.seriesId}}">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.deleted="{ item }">
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@
|
|||
<multi-select-bar
|
||||
v-model="selectedBooks"
|
||||
kind="books"
|
||||
:oneshots="selectedOneshots"
|
||||
@unselect-all="selectedBooks = []"
|
||||
@mark-read="markSelectedBooksRead"
|
||||
@mark-unread="markSelectedBooksUnread"
|
||||
@add-to-readlist="addToReadList"
|
||||
@add-to-collection="addOneshotsToCollection"
|
||||
@edit="editMultipleBooks"
|
||||
@bulk-edit="bulkEditMultipleBooks"
|
||||
@delete="deleteBooks"
|
||||
|
|
@ -165,7 +167,7 @@ import {
|
|||
SERIES_DELETED,
|
||||
} from '@/types/events'
|
||||
import Vue from 'vue'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import {
|
||||
BookSseDto,
|
||||
CollectionSseDto,
|
||||
|
|
@ -264,6 +266,9 @@ export default Vue.extend({
|
|||
this.loaderCollections?.items.length === 0 &&
|
||||
this.loaderReadLists?.items.length === 0
|
||||
},
|
||||
selectedOneshots(): boolean {
|
||||
return this.selectedBooks.every(b => b.oneshot)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async scrollChanged(loader: PageLoader<any>, 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue