mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 16:03:03 +01:00
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:
parent
17c80cd1a1
commit
24c994f840
8 changed files with 171 additions and 14 deletions
|
|
@ -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 () {
|
||||
|
|
|
|||
15
komga-webui/src/functions/book-progress.ts
Normal file
15
komga-webui/src/functions/book-progress.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
komga-webui/src/styles/unread-triangle.css
Normal file
10
komga-webui/src/styles/unread-triangle.css
Normal 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;
|
||||
}
|
||||
|
|
@ -11,3 +11,9 @@ export enum MediaStatus {
|
|||
ERROR = 'ERROR',
|
||||
UNSUPPORTED = 'UNSUPPORTED'
|
||||
}
|
||||
|
||||
export enum ReadProgress {
|
||||
UNREAD = 'UNREAD',
|
||||
READ = 'READ',
|
||||
IN_PROGRESS = 'IN_PROGRESS'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue