feat(webui): mark books as read or unread

available from the book details screen, and from the series screen (for multiple books)

related to #25
This commit is contained in:
Gauthier Roebroeck 2020-06-02 17:28:42 +08:00
parent 17c80cd1a1
commit 24c994f840
8 changed files with 171 additions and 14 deletions

View file

@ -12,6 +12,7 @@
lazy-src="../assets/cover.svg"
aspect-ratio="0.7071"
>
<div class="unread" v-if="isUnread"/>
<v-fade-transition>
<v-overlay
v-if="hover || selected || preselect"
@ -36,6 +37,13 @@
</v-icon>
</v-overlay>
</v-fade-transition>
<v-progress-linear
v-if="isInProgress"
:value="readProgressPercentage"
color="orange"
height="6"
style="position: absolute; bottom: 0"
/>
</v-img>
<!-- Description-->
<v-card-subtitle
@ -52,8 +60,10 @@
</template>
<script lang="ts">
import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress'
import { ReadProgress } from '@/types/enum-books'
import { createItem, Item } from '@/types/items'
import Vue from 'vue'
import { BookItem, createItem, Item } from '@/types/items'
export default Vue.extend({
name: 'ItemCard',
@ -112,6 +122,18 @@ export default Vue.extend({
body (): string {
return this.computedItem.body()
},
isInProgress (): boolean {
if ('seriesId' in this.item) return getReadProgress(this.item) === ReadProgress.IN_PROGRESS
return false
},
isUnread (): boolean {
if ('seriesId' in this.item) return getReadProgress(this.item) === ReadProgress.UNREAD
return false
},
readProgressPercentage (): number {
if ('seriesId' in this.item) return getReadProgressPercentage(this.item)
return 0
},
},
methods: {
onClick () {

View file

@ -0,0 +1,15 @@
import { ReadProgress } from '@/types/enum-books'
export function getReadProgress (book: BookDto): ReadProgress {
if (book.readProgress?.completed) return ReadProgress.READ
if (book.readProgress?.completed === false) return ReadProgress.IN_PROGRESS
return ReadProgress.UNREAD
}
export function getReadProgressPercentage (book: BookDto): number {
if (book.readProgress?.completed) return 100
if (book.readProgress?.completed === false) {
return book.readProgress?.page / book.media.pagesCount * 100
}
return 0
}

View file

@ -119,4 +119,28 @@ export default class KomgaBooksService {
throw new Error(msg)
}
}
async updateReadProgress (bookId: number, readProgress: ReadProgressUpdateDto) {
try {
await this.http.patch(`${API_BOOKS}/${bookId}/read-progress`, readProgress)
} catch (e) {
let msg = `An error occurred while trying to update read progress`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async deleteReadProgress (bookId: number) {
try {
await this.http.delete(`${API_BOOKS}/${bookId}/read-progress`)
} catch (e) {
let msg = `An error occurred while trying to delete read progress`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
}

View file

@ -0,0 +1,10 @@
.unread {
border-left: 25px solid transparent;
border-right: 25px solid orange;
border-bottom: 25px solid transparent;
height: 0;
width: 0;
position: absolute;
right: 0;
z-index: 2;
}

View file

@ -11,3 +11,9 @@ export enum MediaStatus {
ERROR = 'ERROR',
UNSUPPORTED = 'UNSUPPORTED'
}
export enum ReadProgress {
UNREAD = 'UNREAD',
READ = 'READ',
IN_PROGRESS = 'IN_PROGRESS'
}

View file

@ -8,7 +8,8 @@ interface BookDto {
sizeBytes: number,
size: string,
media: MediaDto,
metadata: BookMetadataDto
metadata: BookMetadataDto,
readProgress?: ReadProgressDto
}
interface MediaDto {
@ -47,6 +48,13 @@ interface BookMetadataDto {
authorsLock: boolean,
}
interface ReadProgressDto {
page: number,
completed: boolean,
created: string,
lastModified: string
}
interface BookMetadataUpdateDto {
title?: string,
titleLock?: boolean,
@ -73,6 +81,11 @@ interface AuthorDto {
role: string
}
interface ReadProgressUpdateDto {
page?: number,
completed?: boolean
}
interface BookFormat {
type: string,
color: string

View file

@ -29,6 +29,16 @@
<v-list-item @click="refreshMetadata()">
<v-list-item-title>Refresh metadata</v-list-item-title>
</v-list-item>
<v-list-item
v-if="!isRead"
@click="markRead()">
<v-list-item-title>Mark as read</v-list-item-title>
</v-list-item>
<v-list-item
v-if="!isUnread"
@click="markUnread()">
<v-list-item-title>Mark as unread</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</toolbar-sticky>
@ -43,6 +53,7 @@
max-height="300"
max-width="212"
>
<div class="unread" v-if="isUnread"/>
<v-fade-transition>
<v-overlay
v-if="hover && book.media.status === 'READY'"
@ -58,6 +69,13 @@
</v-btn>
</v-overlay>
</v-fade-transition>
<v-progress-linear
v-if="isInProgress"
:value="readProgressPercentage"
color="orange"
height="6"
style="position: absolute; bottom: 0"
/>
</v-img>
</template>
</v-hover>
@ -175,14 +193,16 @@
</template>
<script lang="ts">
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { getBookFormatFromMediaType } from '@/functions/book-format'
import { bookFileUrl, bookThumbnailUrl } from '@/functions/urls'
import Vue from 'vue'
import { getBookTitleCompact } from '@/functions/book-title'
import Badge from '@/components/Badge.vue'
import EditBooksDialog from '@/components/EditBooksDialog.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { groupAuthorsByRolePlural } from '@/functions/authors'
import { getBookFormatFromMediaType } from '@/functions/book-format'
import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress'
import { getBookTitleCompact } from '@/functions/book-title'
import { bookFileUrl, bookThumbnailUrl } from '@/functions/urls'
import { ReadProgress } from '@/types/enum-books'
import Vue from 'vue'
export default Vue.extend({
name: 'BrowseBook',
@ -234,6 +254,18 @@ export default Vue.extend({
authorsByRole (): any {
return groupAuthorsByRolePlural(this.book.metadata.authors)
},
isRead (): boolean {
return getReadProgress(this.book) === ReadProgress.READ
},
isUnread (): boolean {
return getReadProgress(this.book) === ReadProgress.UNREAD
},
isInProgress (): boolean {
return getReadProgress(this.book) === ReadProgress.IN_PROGRESS
},
readProgressPercentage (): number {
return getReadProgressPercentage(this.book)
},
},
methods: {
analyze () {
@ -242,9 +274,19 @@ export default Vue.extend({
refreshMetadata () {
this.$komgaBooks.refreshMetadata(this.book)
},
async markRead () {
const readProgress = { completed: true } as ReadProgressUpdateDto
await this.$komgaBooks.updateReadProgress(this.book.id, readProgress)
this.book = await this.$komgaBooks.getBook(this.bookId)
},
async markUnread () {
await this.$komgaBooks.deleteReadProgress(this.book.id)
this.book = await this.$komgaBooks.getBook(this.bookId)
},
},
})
</script>
<style scoped>
@import "../styles/unread-triangle.css";
</style>

View file

@ -55,6 +55,14 @@
<v-spacer/>
<v-btn @click="markSelectedRead()">
Mark as read
</v-btn>
<v-btn @click="markSelectedUnread()">
Mark as unread
</v-btn>
<v-btn icon @click="dialogEditBooks = true" v-if="isAdmin">
<v-icon>mdi-pencil</v-icon>
</v-btn>
@ -96,7 +104,8 @@
</v-row>
<v-divider class="my-4"/>
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-6" @update="updateVisible"></item-browser>
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-6"
@update="updateVisible"></item-browser>
</v-container>
<edit-series-dialog v-model="dialogEdit"
:series.sync="series"/>
@ -105,15 +114,15 @@
<script lang="ts">
import Badge from '@/components/Badge.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import Vue from 'vue'
import { parseQuerySort } from '@/functions/query-params'
import { seriesThumbnailUrl } from '@/functions/urls'
import { LoadState } from '@/types/common'
import EditBooksDialog from '@/components/EditBooksDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { parseQuerySort } from '@/functions/query-params'
import { seriesThumbnailUrl } from '@/functions/urls'
import { LoadState } from '@/types/common'
import Vue from 'vue'
export default Vue.extend({
name: 'BrowseSeries',
@ -304,6 +313,22 @@ export default Vue.extend({
this.editBookSingle = book
this.dialogEditBookSingle = true
},
async markSelectedRead () {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.updateReadProgress(b.id, { completed: true }),
))
this.selectedBooks = await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.getBook(b.id),
))
},
async markSelectedUnread () {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.deleteReadProgress(b.id),
))
this.selectedBooks = await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.getBook(b.id),
))
},
},
})
</script>