Add support for bulk reading progress reset

This commit is contained in:
aditya.chandel
2025-07-14 09:18:38 -06:00
committed by Aditya Chandel
parent ec7846abf6
commit 77d68108ab
6 changed files with 133 additions and 25 deletions

View File

@ -1,6 +1,7 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.config.security.annotation.CheckBookAccess;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.model.dto.Book;
import com.adityachandel.booklore.model.dto.BookRecommendation;
import com.adityachandel.booklore.model.dto.BookViewerSettings;
@ -125,10 +126,12 @@ public class BookController {
return ResponseEntity.noContent().build();
}
@PostMapping("/{bookId}/reset-progress")
@CheckBookAccess(bookIdParam = "bookId")
public ResponseEntity<Book> resetProgress(@PathVariable long bookId) {
Book book = bookService.resetProgress(bookId);
return ResponseEntity.ok(book);
@PostMapping("/reset-progress")
public ResponseEntity<List<Book>> resetProgress(@RequestBody List<Long> bookIds) {
if (bookIds == null || bookIds.isEmpty()) {
throw ApiError.BAD_REQUEST.createException("No book IDs provided");
}
List<Book> updatedBooks = bookService.resetProgress(bookIds);
return ResponseEntity.ok(updatedBooks);
}
}

View File

@ -285,22 +285,34 @@ public class BookService {
return book;
}
public Book resetProgress(long bookId) {
public List<Book> resetProgress(List<Long> bookIds) {
BookLoreUser user = authenticationService.getAuthenticatedUser();
BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
List<Book> updatedBooks = new ArrayList<>();
Optional<BookLoreUserEntity> userEntity = userRepository.findById(user.getId());
UserBookProgressEntity progress = userBookProgressRepository.findByUserIdAndBookId(user.getId(), bookId).orElse(new UserBookProgressEntity());
progress.setBook(bookEntity);
progress.setReadStatus(null);
progress.setLastReadTime(null);
progress.setPdfProgress(null);
progress.setPdfProgressPercent(null);
progress.setEpubProgress(null);
progress.setEpubProgressPercent(null);
progress.setCbxProgress(null);
progress.setCbxProgressPercent(null);
userBookProgressRepository.save(progress);
return bookMapper.toBook(bookEntity);
for (Long bookId : bookIds) {
BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
UserBookProgressEntity progress = userBookProgressRepository
.findByUserIdAndBookId(user.getId(), bookId)
.orElse(new UserBookProgressEntity());
progress.setBook(bookEntity);
progress.setUser(userEntity.orElseThrow());
progress.setReadStatus(null);
progress.setLastReadTime(null);
progress.setPdfProgress(null);
progress.setPdfProgressPercent(null);
progress.setEpubProgress(null);
progress.setEpubProgressPercent(null);
progress.setCbxProgress(null);
progress.setCbxProgressPercent(null);
userBookProgressRepository.save(progress);
updatedBooks.add(bookMapper.toBook(bookEntity));
}
return updatedBooks;
}

View File

@ -293,9 +293,9 @@
<div class="flex justify-between items-center">
<div class="flex">
@if (entityType$ | async; as entityType) {
<div class="flex gap-6 pr-2">
<div class="flex gap-2 md:gap-6 pr-2">
@if (userData.permissions.canEditMetadata) {
<p-menu #menu [model]="metadataMenuItems" [popup]="true" appendTo="body"/>
<p-menu #menu [model]="metadataMenuItems" [popup]="true" appendTo="body" class="hidden"/>
<p-button
(click)="menu.toggle($event)"
pTooltip="Metadata actions"
@ -343,10 +343,19 @@
tooltipPosition="top">
</p-button>
}
<p-menu #menuMore [model]="moreActionsMenuItems" [popup]="true" appendTo="body" class="hidden"/>
<p-button
(click)="menuMore.toggle($event)"
pTooltip="More actions"
tooltipPosition="top"
outlined="true"
severity="info"
icon="pi pi-ellipsis-v">
</p-button>
</div>
}
<p-divider layout="vertical"></p-divider>
<div class="flex gap-6 px-2">
<div class="flex gap-2 md:gap-6 px-2">
<p-button
outlined="true"
icon="pi pi-check-square"

View File

@ -149,6 +149,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
lastAppliedSort: SortOption | null = null;
private settingFiltersFromUrl = false;
protected metadataMenuItems: MenuItem[] | undefined;
protected moreActionsMenuItems: MenuItem[] | undefined;
currentBooks: Book[] = [];
lastSelectedIndex: number | null = null;
showFilter: boolean = false;
@ -226,8 +227,28 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
command: () => this.multiBookEditMetadata()
}
];
this.moreActionsMenuItems = [
{
label: 'Reset Progress',
icon: 'pi pi-undo',
command: () => {
this.confirmationService.confirm({
message: 'Are you sure you want to reset progress for selected books?',
header: 'Confirm Reset',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Yes',
rejectLabel: 'No',
accept: () => {
this.resetProgress();
}
});
}
}
];
}
ngAfterViewInit() {
combineLatest({
@ -688,4 +709,26 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
moveFiles() {
this.dialogHelperService.openFileMoverDialog(this.selectedBooks);
}
resetProgress() {
const bookIds = Array.from(this.selectedBooks);
this.bookService.resetProgress(bookIds).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Progress Reset',
detail: 'Reading progress has been reset.',
life: 1500
});
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Failed',
detail: 'Could not reset progress.',
life: 1500
});
}
});
}
}

View File

@ -122,6 +122,7 @@ export class BookCardComponent implements OnInit, OnDestroy {
},
},
...this.getPermissionBasedMenuItems(),
...this.moreMenuItems(),
];
}
@ -258,6 +259,45 @@ export class BookCardComponent implements OnInit, OnDestroy {
return items;
}
private moreMenuItems(): MenuItem[] {
const items: MenuItem[] = [];
if (this.hasEditMetadataPermission()) {
items.push({
label: 'More Actions',
icon: 'pi pi-ellipsis-v',
items: [
{
label: 'Reset Progress',
icon: 'pi pi-undo',
command: () => {
this.bookService.resetProgress(this.book.id).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Progress Reset',
detail: 'Reading progress has been reset.',
life: 1500
});
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Failed',
detail: 'Could not reset progress.',
life: 1500
});
}
});
},
}
]
});
}
return items;
}
private openShelfDialog(): void {
this.dialogService.open(ShelfAssignerComponent, {
header: `Update Book's Shelves`,

View File

@ -397,9 +397,10 @@ export class BookService {
);
}
resetProgress(bookId: number): Observable<Book> {
return this.http.post<Book>(`${this.url}/${bookId}/reset-progress`, null).pipe(
tap(updatedBook => this.handleBookUpdate(updatedBook))
resetProgress(bookIds: number | number[]): Observable<Book[]> {
const ids = Array.isArray(bookIds) ? bookIds : [bookIds];
return this.http.post<Book[]>(`${this.url}/reset-progress`, ids).pipe(
tap(updatedBooks => updatedBooks.forEach(book => this.handleBookUpdate(book)))
);
}