feat(webui): navigate between books of a readlist

This commit is contained in:
Gauthier Roebroeck 2021-01-06 15:12:19 +08:00
parent bcfb203f74
commit 88d4342ef5
28 changed files with 170 additions and 18 deletions

View file

@ -92,6 +92,7 @@ import Vue from 'vue'
import ReadListAddToDialog from '@/components/dialogs/ReadListAddToDialog.vue'
import ReadListDeleteDialog from '@/components/dialogs/ReadListDeleteDialog.vue'
import ReadListEditDialog from '@/components/dialogs/ReadListEditDialog.vue'
import { BookDto } from '@/types/komga-books'
export default Vue.extend({
name: 'Dialogs',

View file

@ -50,7 +50,7 @@
x-large
color="accent"
style="position: absolute; top: 50%; left: 50%; margin-left: -36px; margin-top: -36px"
:to="{name: 'read-book', params: { bookId: item.id}}"
:to="fabTo"
>
<v-icon>mdi-book-open-page-variant</v-icon>
</v-btn>
@ -124,6 +124,7 @@ import { createItem, Item, ItemTypes } from '@/types/items'
import Vue from 'vue'
import { RawLocation } from 'vue-router'
import ReadListActionsMenu from '@/components/menus/ReadListActionsMenu.vue'
import { BookDto } from '@/types/komga-books'
export default Vue.extend({
name: 'ItemCard',
@ -232,6 +233,9 @@ export default Vue.extend({
to (): RawLocation {
return this.computedItem.to()
},
fabTo (): RawLocation {
return this.computedItem.fabTo()
},
},
methods: {
onClick () {

View file

@ -30,6 +30,8 @@
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import Vue from 'vue'
import { BookDto } from '@/types/komga-books'
import { ContextOrigin } from '@/types/context'
export default Vue.extend({
name: 'ReadListsExpansionPanels',
@ -62,6 +64,7 @@ export default Vue.extend({
const rlId = this.readLists[val].id
if (this.$_.isEmpty(this.readListsContent[val])) {
const content = (await this.$komgaReadLists.getBooks(rlId, { unpaged: true } as PageRequest)).content
content.forEach((x: BookDto) => x.context = { origin: ContextOrigin.READLIST, id: rlId })
this.readListsContent.splice(val, 1, content)
}
}

View file

@ -105,6 +105,7 @@
import { bookThumbnailUrl, collectionThumbnailUrl, readListThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls'
import { debounce } from 'lodash'
import Vue from 'vue'
import { BookDto } from '@/types/komga-books'
export default Vue.extend({
name: 'SearchBox',

View file

@ -279,6 +279,7 @@ import { authorRoles } from '@/types/author-roles'
import moment from 'moment'
import Vue from 'vue'
import { helpers, requiredIf } from 'vuelidate/lib/validators'
import { BookDto } from '@/types/komga-books'
const validDate = (value: any) => !helpers.req(value) || moment(value, 'YYYY-MM-DD', true).isValid()

View file

@ -80,6 +80,7 @@
<script lang="ts">
import Vue from 'vue'
import { BookDto } from '@/types/komga-books'
export default Vue.extend({
name: 'ReadListAddToDialog',

View file

@ -31,6 +31,7 @@ import { getReadProgress } from '@/functions/book-progress'
import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED, bookToEventBookChanged } from '@/types/events'
import Vue from 'vue'
import { BookDto, ReadProgressUpdateDto } from '@/types/komga-books'
export default Vue.extend({
name: 'BookActionsMenu',

View file

@ -38,6 +38,7 @@
<script lang="ts">
import Vue from 'vue'
import { ContinuousScaleType } from '@/types/enum-reader'
import { PageDtoWithUrl } from '@/types/komga-books'
export default Vue.extend({
name: 'ContinuousReader',

View file

@ -80,6 +80,7 @@ import Vue from 'vue'
import { ReadingDirection } from '@/types/enum-books'
import { PagedReaderLayout, ScaleType } from '@/types/enum-reader'
import { shortcutsLTR, shortcutsRTL, shortcutsVertical } from '@/functions/shortcuts/paged-reader'
import { PageDtoWithUrl } from '@/types/komga-books'
export default Vue.extend({
name: 'PagedReader',

View file

@ -1,5 +1,6 @@
import { get, groupBy, mapKeys, mapValues } from 'lodash'
import { authorRoles } from '@/types/author-roles'
import { AuthorDto } from '@/types/komga-books'
// return an object where keys are roles, and values are string[]
export function groupAuthorsByRole (authors: AuthorDto[]): any {

View file

@ -1,3 +1,5 @@
import { BookFormat } from '@/types/komga-books'
export function getBookFormatFromMediaType (mediaType: string): BookFormat {
switch (mediaType) {
case 'application/x-rar-compressed':

View file

@ -1,4 +1,5 @@
import { ReadStatus } from '@/types/enum-books'
import { BookDto } from '@/types/komga-books'
export function getReadProgress (book: BookDto): ReadStatus {
if (book.readProgress?.completed) return ReadStatus.READ

View file

@ -1,3 +1,5 @@
import { PageDto } from '@/types/komga-books'
export function isPageLandscape (p: PageDto): boolean {
return (p?.width ?? 0) > (p?.height ?? 0)
}

View file

@ -1,4 +1,5 @@
import { AxiosInstance } from 'axios'
import { BookDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto } from '@/types/komga-books'
const qs = require('qs')

View file

@ -1,4 +1,5 @@
import { AxiosInstance } from 'axios'
import { BookDto } from '@/types/komga-books'
const qs = require('qs')
@ -92,4 +93,28 @@ export default class KomgaReadListsService {
throw new Error(msg)
}
}
async getBookSiblingNext (readListId: string, bookId: string): Promise<BookDto> {
try {
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/next`)).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve book'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getBookSiblingPrevious (readListId: string, bookId: string): Promise<BookDto> {
try {
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/previous`)).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve book'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
}

View file

@ -1,4 +1,5 @@
import { AxiosInstance } from 'axios'
import { BookDto } from '@/types/komga-books'
const qs = require('qs')

View file

@ -1,5 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { BookDto } from '@/types/komga-books'
Vue.use(Vuex)

View file

@ -0,0 +1,9 @@
export enum ContextOrigin {
SERIES = 'SERIES',
READLIST = 'READLIST'
}
export interface Context {
origin: ContextOrigin,
id: string,
}

View file

@ -1,3 +1,5 @@
import { BookDto } from '@/types/komga-books'
export const BOOK_CHANGED = 'book-changed'
export const SERIES_CHANGED = 'series-changed'
export const COLLECTION_DELETED = 'collection-deleted'

View file

@ -1,5 +1,6 @@
import { bookThumbnailUrl, collectionThumbnailUrl, readListThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls'
import { RawLocation } from 'vue-router/types/router'
import { BookDto } from '@/types/komga-books'
function plural (count: number, singular: string, plural: string) {
return `${count} ${count === 1 ? singular : plural}`
@ -47,6 +48,8 @@ export abstract class Item<T> {
abstract body (): string
abstract to (): RawLocation
abstract fabTo (): RawLocation
}
export class BookItem extends Item<BookDto> {
@ -69,7 +72,19 @@ export class BookItem extends Item<BookDto> {
}
to (): RawLocation {
return { name: 'browse-book', params: { bookId: this.item.id.toString() } }
return {
name: 'browse-book',
params: { bookId: this.item.id },
query: { context: this.item?.context?.origin, contextId: this.item?.context?.id },
}
}
fabTo (): RawLocation {
return {
name: 'read-book',
params: { bookId: this.item.id },
query: { context: this.item?.context?.origin, contextId: this.item?.context?.id },
}
}
}
@ -94,6 +109,10 @@ export class SeriesItem extends Item<SeriesDto> {
to (): RawLocation {
return { name: 'browse-series', params: { seriesId: this.item.id.toString() } }
}
fabTo (): RawLocation {
return undefined as unknown as RawLocation
}
}
export class CollectionItem extends Item<CollectionDto> {
@ -117,6 +136,10 @@ export class CollectionItem extends Item<CollectionDto> {
to (): RawLocation {
return { name: 'browse-collection', params: { collectionId: this.item.id.toString() } }
}
fabTo (): RawLocation {
return undefined as unknown as RawLocation
}
}
export class ReadListItem extends Item<ReadListDto> {
@ -140,4 +163,8 @@ export class ReadListItem extends Item<ReadListDto> {
to (): RawLocation {
return { name: 'browse-readlist', params: { readListId: this.item.id.toString() } }
}
fabTo (): RawLocation {
return undefined as unknown as RawLocation
}
}

View file

@ -1,4 +1,6 @@
interface BookDto {
import { Context } from '@/types/context'
export interface BookDto {
id: string,
seriesId: string,
libraryId: string,
@ -11,16 +13,19 @@ interface BookDto {
media: MediaDto,
metadata: BookMetadataDto,
readProgress?: ReadProgressDto
// custom fields
context: Context
}
interface MediaDto {
export interface MediaDto {
status: string,
mediaType: string,
pagesCount: number,
comment: string
}
interface PageDto {
export interface PageDto {
number: number,
fileName: string,
mediaType: string,
@ -28,7 +33,7 @@ interface PageDto {
height?: number,
}
interface PageDtoWithUrl {
export interface PageDtoWithUrl {
number: number,
fileName: string,
mediaType: string,
@ -37,7 +42,7 @@ interface PageDtoWithUrl {
url: string,
}
interface BookMetadataDto {
export interface BookMetadataDto {
created: string,
lastModified: string,
title: string,
@ -56,14 +61,14 @@ interface BookMetadataDto {
tagsLock: boolean
}
interface ReadProgressDto {
export interface ReadProgressDto {
page: number,
completed: boolean,
created: string,
lastModified: string
}
interface BookMetadataUpdateDto {
export interface BookMetadataUpdateDto {
title?: string,
titleLock?: boolean,
summary?: string,
@ -80,17 +85,17 @@ interface BookMetadataUpdateDto {
tagsLock?: boolean
}
interface AuthorDto {
export interface AuthorDto {
name: string,
role: string
}
interface ReadProgressUpdateDto {
export interface ReadProgressUpdateDto {
page?: number,
completed?: boolean
}
interface BookFormat {
export interface BookFormat {
type: string,
color: string
}

View file

@ -288,6 +288,7 @@ import {
import { shortcutsMenus, shortcutsSettings } from '@/functions/shortcuts/bookreader'
import { shortcutsAll } from '@/functions/shortcuts/reader'
import { shortcutsSettingsContinuous } from '@/functions/shortcuts/continuous-reader'
import { BookDto, PageDto, PageDtoWithUrl } from '@/types/komga-books'
const cookieFit = 'webreader.fit'
const cookieContinuousReaderFit = 'webreader.continuousReaderFit'

View file

@ -12,14 +12,28 @@
<v-spacer/>
<!-- Context notification for navigation -->
<v-alert
v-if="contextReadList"
type="info"
text
dense
border="right"
class="mb-0"
>
Navigation within the readlist: {{ contextName }}
</v-alert>
<!-- Navigate to previous book -->
<v-btn
icon
:disabled="$_.isEmpty(siblingPrevious)"
:to="{ name: 'browse-book', params: { bookId: previousId } }"
:to="{ name: 'browse-book', params: { bookId: previousId }, query: { context: context.origin, contextId: context.id} }"
>
<v-icon>mdi-chevron-left</v-icon>
</v-btn>
<!-- List of all books in context (series/readlist) for navigation -->
<v-menu bottom
offset-y
:max-height="$vuetify.breakpoint.height * .7"
@ -38,7 +52,7 @@
<v-list-item
v-for="(book, i) in siblings"
:key="i"
:to="{ name: 'browse-book', params: { bookId: book.id } }"
:to="{ name: 'browse-book', params: { bookId: book.id }, query: { context: context.origin, contextId: context.id} }"
>
<v-list-item-title class="text-wrap text-body-2">{{ book.metadata.number }} - {{ book.metadata.title }}
</v-list-item-title>
@ -47,10 +61,11 @@
</v-list>
</v-menu>
<!-- Navigate to next book -->
<v-btn
icon
:disabled="$_.isEmpty(siblingNext)"
:to="{ name: 'browse-book', params: { bookId: nextId } }"
:to="{ name: 'browse-book', params: { bookId: nextId }, query: { context: context.origin, contextId: context.id} }"
>
<v-icon>mdi-chevron-right</v-icon>
</v-btn>
@ -212,6 +227,8 @@ import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED, LIBRARY_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'
export default Vue.extend({
name: 'BrowseBook',
@ -220,6 +237,8 @@ export default Vue.extend({
return {
book: {} as BookDto,
series: {} as SeriesDto,
context: {} as Context,
contextName: '',
siblings: [] as BookDto[],
siblingPrevious: {} as BookDto,
siblingNext: {} as BookDto,
@ -288,6 +307,9 @@ export default Vue.extend({
nextId (): string {
return this.siblingNext?.id?.toString() || '0'
},
contextReadList (): boolean {
return this.context.origin === ContextOrigin.READLIST
},
},
methods: {
libraryDeleted (event: EventLibraryDeleted) {
@ -301,7 +323,30 @@ export default Vue.extend({
async loadBook (bookId: string) {
this.book = await this.$komgaBooks.getBook(bookId)
this.series = await this.$komgaSeries.getOneSeries(this.book.seriesId)
this.siblings = (await this.$komgaSeries.getBooks(this.book.seriesId, { unpaged: true } as PageRequest)).content
// 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
this.contextName = (await (this.$komgaReadLists.getOneReadList(this.context.id))).name
} else {
this.context = {
origin: ContextOrigin.SERIES,
id: this.book.seriesId,
}
}
// Get siblings depending on origin
if (this?.context.origin === ContextOrigin.SERIES) {
this.siblings = (await this.$komgaSeries.getBooks(this.book.seriesId, { unpaged: true } as PageRequest)).content
} else if (this.context.origin === ContextOrigin.READLIST) {
this.siblings = (await this.$komgaReadLists.getBooks(this.context.id, { unpaged: true } as PageRequest)).content
}
this.readLists = await this.$komgaBooks.getReadLists(this.bookId)
if (this.$_.has(this.book, 'metadata.title')) {
@ -309,12 +354,20 @@ export default Vue.extend({
}
try {
this.siblingNext = await this.$komgaBooks.getBookSiblingNext(bookId)
if (this?.context.origin === ContextOrigin.SERIES) {
this.siblingNext = await this.$komgaBooks.getBookSiblingNext(bookId)
} else if (this.context.origin === ContextOrigin.READLIST) {
this.siblingNext = await this.$komgaReadLists.getBookSiblingNext(this.context.id, bookId)
}
} catch (e) {
this.siblingNext = {} as BookDto
}
try {
this.siblingPrevious = await this.$komgaBooks.getBookSiblingPrevious(bookId)
if (this?.context.origin === ContextOrigin.SERIES) {
this.siblingPrevious = await this.$komgaBooks.getBookSiblingPrevious(bookId)
} else if (this.context.origin === ContextOrigin.READLIST) {
this.siblingPrevious = await this.$komgaReadLists.getBookSiblingPrevious(this.context.id, bookId)
}
} catch (e) {
this.siblingPrevious = {} as BookDto
}

View file

@ -80,6 +80,8 @@ import { BOOK_CHANGED, READLIST_CHANGED, READLIST_DELETED } from '@/types/events
import Vue from 'vue'
import ReadListActionsMenu from '@/components/menus/ReadListActionsMenu.vue'
import BooksMultiSelectBar from '@/components/bars/BooksMultiSelectBar.vue'
import { BookDto, ReadProgressUpdateDto } from '@/types/komga-books'
import { ContextOrigin } from '@/types/context'
export default Vue.extend({
name: 'BrowseReadList',
@ -156,6 +158,7 @@ export default Vue.extend({
async loadReadList (readListId: string) {
this.readList = await this.$komgaReadLists.getOneReadList(readListId)
this.books = (await this.$komgaReadLists.getBooks(readListId, { unpaged: true } as PageRequest)).content
this.books.forEach((x: BookDto) => x.context = { origin: ContextOrigin.READLIST, id: readListId })
this.booksCopy = [...this.books]
this.selectedBooks = []
},

View file

@ -205,6 +205,7 @@ import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'
import { Location } from 'vue-router'
import { BookDto } from '@/types/komga-books'
import { SeriesStatus } from '@/types/enum-series'
import FilterDrawer from '@/components/FilterDrawer.vue'
import FilterList from '@/components/FilterList.vue'

View file

@ -111,6 +111,7 @@ import EmptyState from '@/components/EmptyState.vue'
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import { ReadStatus } from '@/types/enum-books'
import { BookDto } from '@/types/komga-books'
import { BOOK_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'

View file

@ -107,6 +107,7 @@ import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import EmptyState from '@/components/EmptyState.vue'
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import { BookDto } from '@/types/komga-books'
import {
BOOK_CHANGED,
COLLECTION_CHANGED,

View file

@ -23,6 +23,7 @@
<script lang="ts">
import Vue from 'vue'
import { MediaStatus } from '@/types/enum-books'
import { BookDto } from '@/types/komga-books'
export default Vue.extend({
name: 'SettingsMediaAnalysis',