mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Added late read module
This commit is contained in:
@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
@RequestMapping("/v1/book")
|
||||
@RestController
|
||||
@@ -30,9 +31,13 @@ public class BookController {
|
||||
return ResponseEntity.ok(booksService.getBook(bookId));
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
public ResponseEntity<Page<BookDTO>> getBooks(@RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "25") @Min(1) @Max(100) int size) {
|
||||
Page<BookDTO> books = booksService.getBooks(page, size);
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<BookDTO>> getBooks(
|
||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
||||
@RequestParam(defaultValue = "25") @Min(1) @Max(100) int size,
|
||||
@RequestParam(defaultValue = "lastReadTime") String sortBy,
|
||||
@RequestParam(defaultValue = "desc") String sortDir) {
|
||||
Page<BookDTO> books = booksService.getBooks(page, size, sortBy, sortDir);
|
||||
return ResponseEntity.ok(books);
|
||||
}
|
||||
|
||||
@@ -44,6 +49,14 @@ public class BookController {
|
||||
|
||||
@GetMapping("/{bookId}/cover")
|
||||
public ResponseEntity<Resource> getBookCover(@PathVariable long bookId) {
|
||||
Random random = new Random();
|
||||
int delay = 250 + random.nextInt(750);
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
return ResponseEntity.ok(booksService.getBookCover(bookId));
|
||||
}
|
||||
|
||||
@@ -52,9 +65,20 @@ public class BookController {
|
||||
return booksService.getBookData(bookId);
|
||||
}
|
||||
|
||||
@GetMapping("/{bookId}/viewer-setting")
|
||||
public ResponseEntity<BookViewerSettingDTO> getBookViewerSettings(@PathVariable long bookId) {
|
||||
return ResponseEntity.ok(booksService.getBookViewerSetting(bookId));
|
||||
}
|
||||
|
||||
@PutMapping("/{bookId}/viewer-setting")
|
||||
public ResponseEntity<Void> updateBookViewerSettings(@RequestBody BookViewerSettingDTO bookViewerSettingDTO, @PathVariable long bookId) {
|
||||
booksService.saveBookViewerSetting(bookId, bookViewerSettingDTO);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{bookId}/update-last-read")
|
||||
public ResponseEntity<Void> updateBookViewerSettings(@PathVariable long bookId) {
|
||||
booksService.updateLastReadTime(bookId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.adityachandel.booklore.dto;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -13,5 +14,6 @@ public class BookDTO {
|
||||
private Long libraryId;
|
||||
private String fileName;
|
||||
private String title;
|
||||
private Instant lastReadTime;
|
||||
private List<AuthorDTO> authors = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.adityachandel.booklore.entity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@@ -40,4 +41,7 @@ public class Book {
|
||||
|
||||
@OneToOne(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private BookViewerSetting viewerSetting;
|
||||
|
||||
@Column(name = "last_read_time")
|
||||
private Instant lastReadTime;
|
||||
}
|
||||
|
||||
@@ -19,5 +19,7 @@ public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificat
|
||||
Optional<Book> findBookByIdAndLibraryId(long id, long libraryId);
|
||||
|
||||
List<Book> findByTitleContainingIgnoreCase(String title);
|
||||
|
||||
Page<Book> findByLastReadTimeIsNotNull(Pageable pageable);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.adityachandel.booklore.exception.ErrorCode;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.repository.BookViewerSettingRepository;
|
||||
import com.adityachandel.booklore.service.parser.PdfParser;
|
||||
import com.adityachandel.booklore.transformer.BookSettingTransformer;
|
||||
import com.adityachandel.booklore.transformer.BookTransformer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -18,6 +19,7 @@ import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -28,6 +30,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -49,10 +52,13 @@ public class BooksService {
|
||||
return BookTransformer.convertToBookDTO(book);
|
||||
}
|
||||
|
||||
public Page<BookDTO> getBooks(int page, int size) {
|
||||
PageRequest pageRequest = PageRequest.of(page, size);
|
||||
Page<Book> bookPage = bookRepository.findAll(PageRequest.of(page, size));
|
||||
List<BookDTO> bookDTOs = bookPage.getContent().stream().map(BookTransformer::convertToBookDTO).collect(Collectors.toList());
|
||||
public Page<BookDTO> getBooks(int page, int size, String sortBy, String sortDir) {
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
|
||||
PageRequest pageRequest = PageRequest.of(page, size, sort);
|
||||
Page<Book> bookPage = bookRepository.findByLastReadTimeIsNotNull(pageRequest);
|
||||
List<BookDTO> bookDTOs = bookPage.getContent().stream()
|
||||
.map(BookTransformer::convertToBookDTO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageImpl<>(bookDTOs, pageRequest, bookPage.getTotalElements());
|
||||
}
|
||||
|
||||
@@ -102,10 +108,6 @@ public class BooksService {
|
||||
Book book = pdfParser.parseBook(filePath.toAbsolutePath().toString(), appProperties.getPathConfig());
|
||||
book.setViewerSetting(BookViewerSetting.builder()
|
||||
.bookId(book.getId())
|
||||
.pageNumber(0)
|
||||
.zoom("page-fit")
|
||||
.spread("off")
|
||||
.sidebar_visible(false)
|
||||
.build());
|
||||
return book;
|
||||
}
|
||||
@@ -147,4 +149,15 @@ public class BooksService {
|
||||
List<Book> books = bookRepository.findByTitleContainingIgnoreCase(title);
|
||||
return books.stream().map(BookTransformer::convertToBookDTO).toList();
|
||||
}
|
||||
|
||||
public BookViewerSettingDTO getBookViewerSetting(long bookId) {
|
||||
BookViewerSetting bookViewerSetting = bookViewerSettingRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
|
||||
return BookSettingTransformer.convertToDTO(bookViewerSetting);
|
||||
}
|
||||
|
||||
public void updateLastReadTime(long bookId) {
|
||||
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
|
||||
book.setLastReadTime(Instant.now());
|
||||
bookRepository.save(book);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.adityachandel.booklore.transformer;
|
||||
|
||||
import com.adityachandel.booklore.dto.BookViewerSettingDTO;
|
||||
import com.adityachandel.booklore.entity.BookViewerSetting;
|
||||
|
||||
public class BookSettingTransformer {
|
||||
|
||||
public static BookViewerSettingDTO convertToDTO(BookViewerSetting bookViewerSetting) {
|
||||
return BookViewerSettingDTO.builder()
|
||||
.zoom(bookViewerSetting.getZoom())
|
||||
.pageNumber(bookViewerSetting.getPageNumber())
|
||||
.spread(bookViewerSetting.getSpread())
|
||||
.sidebar_visible(bookViewerSetting.isSidebar_visible())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public class BookTransformer {
|
||||
bookDTO.setLibraryId(book.getLibrary().getId());
|
||||
bookDTO.setFileName(book.getFileName());
|
||||
bookDTO.setTitle(book.getTitle());
|
||||
bookDTO.setLastReadTime(book.getLastReadTime());
|
||||
bookDTO.setAuthors(book.getAuthors().stream().map(AuthorTransformer::toAuthorDTO).collect(Collectors.toList()));
|
||||
return bookDTO;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@ CREATE TABLE IF NOT EXISTS library
|
||||
|
||||
CREATE TABLE IF NOT EXISTS book
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
library_id BIGINT NOT NULL,
|
||||
path VARCHAR(1000) NOT NULL,
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
library_id BIGINT NOT NULL,
|
||||
path VARCHAR(1000) NOT NULL,
|
||||
last_read_time TIMESTAMP NULL,
|
||||
CONSTRAINT fk_library FOREIGN KEY (library_id) REFERENCES library (id) ON DELETE CASCADE,
|
||||
CONSTRAINT unique_file_library UNIQUE (file_name, library_id)
|
||||
CONSTRAINT unique_file_library UNIQUE (file_name, library_id),
|
||||
INDEX idx_library_id (library_id),
|
||||
INDEX idx_last_read_time (last_read_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS author
|
||||
@@ -29,15 +32,17 @@ CREATE TABLE IF NOT EXISTS book_author_mapping
|
||||
author_id BIGINT NOT NULL,
|
||||
CONSTRAINT fk_book_author_mapping_book FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_book_author_mapping_author FOREIGN KEY (author_id) REFERENCES author (id),
|
||||
CONSTRAINT unique_book_author UNIQUE (book_id, author_id)
|
||||
CONSTRAINT unique_book_author UNIQUE (book_id, author_id),
|
||||
INDEX idx_book_id (book_id),
|
||||
INDEX idx_author_id (author_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS book_viewer_setting
|
||||
(
|
||||
book_id BIGINT PRIMARY KEY,
|
||||
page_number INT DEFAULT 1,
|
||||
zoom VARCHAR(32) DEFAULT 'page-fit',
|
||||
zoom VARCHAR(16) DEFAULT 'page-fit',
|
||||
sidebar_visible BOOLEAN DEFAULT false,
|
||||
spread VARCHAR(32) DEFAULT 'off',
|
||||
spread VARCHAR(16) DEFAULT 'odd',
|
||||
CONSTRAINT fk_book_viewer_setting FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -17,9 +17,9 @@ import {InputIconModule} from 'primeng/inputicon';
|
||||
import {ToggleButtonModule} from 'primeng/togglebutton';
|
||||
import {PasswordModule} from 'primeng/password';
|
||||
import {ToastModule} from 'primeng/toast';
|
||||
import { LibraryBrowserComponent } from './book/component/library-browser/library-browser.component';
|
||||
import {LibraryBrowserComponent} from './book/component/library-browser/library-browser.component';
|
||||
import {InfiniteScrollDirective} from 'ngx-infinite-scroll';
|
||||
import { SearchComponent } from './book/component/search/search.component';
|
||||
import {SearchComponent} from './book/component/search/search.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="book-list-container">
|
||||
<h2 class="last-read-title">Last Read</h2>
|
||||
<div class="book-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="loadMore()"
|
||||
#scrollContainer
|
||||
(scroll)="onScroll($event)">
|
||||
<div class="book-item" *ngFor="let book of books">
|
||||
<img [src]="coverImageSrc(book.id)" class="book-cover" alt="Cover of {{ book.title }}" loading="lazy"/>
|
||||
<div class="book-info">
|
||||
<h4>{{ book.title }}</h4>
|
||||
<p>{{ getAuthorNames(book) }}</p>
|
||||
<p-button label="View" icon="pi pi-eye" class="view-btn" (click)="openBook(book.id)"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.book-list-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.last-read-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.book-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
padding: 10px;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.book-item {
|
||||
overflow: hidden;
|
||||
background-color: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
flex: 0 0 12%;
|
||||
scroll-snap-align: start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
height: 225px;
|
||||
object-fit: cover;
|
||||
background-color: #444;
|
||||
transition: background-color 0.3s ease, filter 0.3s ease;
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.book-item:hover .book-cover {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.book-info {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.book-item:hover .view-btn {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Book } from '../../model/book.model';
|
||||
import { BookService } from '../../service/book.service';
|
||||
import { Button } from 'primeng/button';
|
||||
import { InfiniteScrollDirective } from 'ngx-infinite-scroll';
|
||||
import { NgForOf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-scroller',
|
||||
templateUrl: './dashboard-scroller.component.html',
|
||||
imports: [
|
||||
Button,
|
||||
InfiniteScrollDirective,
|
||||
NgForOf
|
||||
],
|
||||
styleUrls: ['./dashboard-scroller.component.scss']
|
||||
})
|
||||
export class DashboardScrollerComponent implements OnInit {
|
||||
books: Book[] = [];
|
||||
private currentPage: number = 0;
|
||||
|
||||
@ViewChild('scrollContainer') scrollContainer!: ElementRef;
|
||||
|
||||
constructor(private bookService: BookService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadBooks();
|
||||
}
|
||||
|
||||
loadBooks(): void {
|
||||
this.bookService.loadLatestBooks(this.currentPage).subscribe({
|
||||
next: (response) => {
|
||||
this.books = [...this.books, ...response.content];
|
||||
this.currentPage++;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error loading books:', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
coverImageSrc(bookId: number): string {
|
||||
return this.bookService.getBookCoverUrl(bookId);
|
||||
}
|
||||
|
||||
loadMore(): void {
|
||||
console.log('Loading more books...');
|
||||
this.loadBooks();
|
||||
}
|
||||
|
||||
getAuthorNames(book: Book): string {
|
||||
return book.authors?.map((author) => author.name).join(', ') || 'No authors available';
|
||||
}
|
||||
|
||||
openBook(bookId: number): void {
|
||||
const url = `/pdf-viewer/book/${bookId}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
|
||||
onScroll(event: any): void {
|
||||
const container = this.scrollContainer.nativeElement;
|
||||
const scrollRight = container.scrollWidth - container.scrollLeft === container.clientWidth;
|
||||
if (scrollRight) {
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
<div class="dashboard">
|
||||
<h1 *ngIf="isLibrariesEmpty" class="no-library-header">
|
||||
Looks like you haven't added a library yet. Let's add one!
|
||||
</h1>
|
||||
<p-button *ngIf="isLibrariesEmpty"
|
||||
label="Add a Library"
|
||||
icon="pi pi-plus"
|
||||
styleClass="p-button-rounded p-button-outlined"
|
||||
(click)="createNewLibrary($event)">
|
||||
</p-button>
|
||||
<div *ngIf="isLibrariesEmpty; else dashboardScroller">
|
||||
<h1 class="no-library-header">
|
||||
Looks like you haven't added a library yet. Let's add one!
|
||||
</h1>
|
||||
<p-button
|
||||
label="Add a Library"
|
||||
icon="pi pi-plus"
|
||||
styleClass="p-button-rounded p-button-outlined"
|
||||
(click)="createNewLibrary($event)">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<ng-template #dashboardScroller>
|
||||
<app-dashboard-scroller></app-dashboard-scroller>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-library-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color)
|
||||
}
|
||||
.p-dialog {
|
||||
max-width: 90%; /* Ensure the dialog doesn't overflow the viewport */
|
||||
}
|
||||
.dashboard .no-library-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.dashboard .p-dialog {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
::ng-deep .p-dialog-mask {
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Darken the background with 50% opacity */
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* You can also adjust the blur if you want */
|
||||
::ng-deep .p-dialog-mask {
|
||||
backdrop-filter: blur(3px); /* Optional: Apply blur to the background */
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ import { Button } from 'primeng/button';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { LibraryCreatorComponent } from '../library-creator/library-creator.component';
|
||||
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||
import {DashboardScrollerComponent} from '../dashboard-scroller/dashboard-scroller.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home-page',
|
||||
templateUrl: './dashboard.component.html',
|
||||
imports: [
|
||||
Button,
|
||||
NgIf
|
||||
NgIf,
|
||||
DashboardScrollerComponent
|
||||
],
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
providers: [DialogService], // Ensure the DialogService is available
|
||||
providers: [DialogService],
|
||||
})
|
||||
export class DashboardComponent {
|
||||
private libraries: WritableSignal<Library[]>;
|
||||
@@ -25,7 +27,7 @@ export class DashboardComponent {
|
||||
}
|
||||
|
||||
get isLibrariesEmpty(): boolean {
|
||||
return this.libraries()?.length === 1;
|
||||
return this.libraries()?.length === 0;
|
||||
}
|
||||
|
||||
createNewLibrary(event: MouseEvent) {
|
||||
@@ -39,12 +41,12 @@ export class DashboardComponent {
|
||||
|
||||
this.ref = this.dialogService.open(LibraryCreatorComponent, {
|
||||
modal: true,
|
||||
width: `${dialogWidthPercentage}%`, // Dynamic width
|
||||
height: 'auto', // Let height adapt to content
|
||||
width: `${dialogWidthPercentage}%`,
|
||||
height: 'auto',
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: `${buttonRect.bottom + 10}px`, // Position below the button
|
||||
left: `${Math.max(leftPosition, 0)}px`, // Ensure it stays within the viewport
|
||||
top: `${buttonRect.bottom + 10}px`,
|
||||
left: `${Math.max(leftPosition, 0)}px`
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<div class="book-list" infiniteScroll [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||
<div class="book-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="50"
|
||||
[horizontal]=true
|
||||
(scrolled)="loadMore()">
|
||||
<div class="book-item" *ngFor="let book of books">
|
||||
<img [src]="coverImageSrc(book.id)" class="book-cover placeholder" alt="Cover of {{ book.title }}" loading="lazy"/>
|
||||
<div class="book-info">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NgxExtendedPdfViewerModule, ScrollModeType } from 'ngx-extended-pdf-viewer';
|
||||
import { BookService } from '../../service/book.service';
|
||||
@@ -9,7 +9,7 @@ import { BookService } from '../../service/book.service';
|
||||
imports: [NgxExtendedPdfViewerModule],
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
})
|
||||
export class PdfViewerComponent implements OnInit {
|
||||
export class PdfViewerComponent implements OnInit, OnDestroy {
|
||||
bookId!: number;
|
||||
handTool = true;
|
||||
page = 1;
|
||||
@@ -31,18 +31,25 @@ export class PdfViewerComponent implements OnInit {
|
||||
this.route.paramMap.subscribe((params) => {
|
||||
this.bookId = +params.get('bookId')!;
|
||||
this.loadBook(this.bookId);
|
||||
this.updateLastReadTime();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.updateLastReadTime();
|
||||
}
|
||||
|
||||
private loadBook(bookId: number): void {
|
||||
this.bookService.getBook(bookId).subscribe((book) => {
|
||||
this.zone.run(() => {
|
||||
const { pageNumber, zoom, sidebar_visible, spread } = book.viewerSetting;
|
||||
this.page = pageNumber || 1;
|
||||
this.zoom = zoom || 'page-fit';
|
||||
this.sidebarVisible = sidebar_visible || false;
|
||||
this.spread = spread || 'odd';
|
||||
this.isInitialLoad = false;
|
||||
this.bookService.getBookSetting(bookId).subscribe((bookSetting) => {
|
||||
const { pageNumber, zoom, sidebar_visible, spread } = bookSetting;
|
||||
this.page = pageNumber || 1;
|
||||
this.zoom = zoom || 'page-fit';
|
||||
this.sidebarVisible = sidebar_visible || false;
|
||||
this.spread = spread || 'odd';
|
||||
this.isInitialLoad = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -58,7 +65,12 @@ export class PdfViewerComponent implements OnInit {
|
||||
this.bookService.updateViewerSetting(updatedViewerSetting, this.bookId).subscribe();
|
||||
}
|
||||
|
||||
private updateLastReadTime(): void {
|
||||
this.bookService.updateLastReadTime(this.bookId).subscribe();
|
||||
}
|
||||
|
||||
onPageChange(page: number): void {
|
||||
console.log('page', page);
|
||||
if (page !== this.page) {
|
||||
this.page = page;
|
||||
this.updateViewerSetting();
|
||||
@@ -66,6 +78,7 @@ export class PdfViewerComponent implements OnInit {
|
||||
}
|
||||
|
||||
onZoomChange(zoom: string | number): void {
|
||||
console.log('zoom', zoom);
|
||||
if (zoom !== this.zoom) {
|
||||
this.zoom = zoom;
|
||||
this.updateViewerSetting();
|
||||
@@ -73,6 +86,7 @@ export class PdfViewerComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSidebarVisibleChange(visible: boolean): void {
|
||||
console.log('sidebarVisible', visible);
|
||||
if (visible !== this.sidebarVisible) {
|
||||
this.sidebarVisible = visible;
|
||||
this.updateViewerSetting();
|
||||
|
||||
6
booklore-ui/src/app/book/model/book-settings.model.ts
Normal file
6
booklore-ui/src/app/book/model/book-settings.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface BookSetting {
|
||||
pageNumber: number;
|
||||
zoom: number | string;
|
||||
sidebar_visible: boolean;
|
||||
spread: 'off' | 'even' | 'odd';
|
||||
}
|
||||
@@ -5,14 +5,6 @@ export interface Book {
|
||||
libraryId: number;
|
||||
title: string;
|
||||
authors: Author[];
|
||||
viewerSetting: BookViewerSetting;
|
||||
}
|
||||
|
||||
export interface BookViewerSetting {
|
||||
pageNumber: number;
|
||||
zoom: number | string;
|
||||
sidebar_visible: boolean;
|
||||
spread: 'off' | 'even' | 'odd';
|
||||
}
|
||||
|
||||
export interface PaginatedBooksResponse {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Observable } from 'rxjs';
|
||||
import { Book, PaginatedBooksResponse } from '../model/book.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {BookSetting} from '../model/book-settings.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -23,6 +24,12 @@ export class BookService {
|
||||
);
|
||||
}
|
||||
|
||||
loadLatestBooks(page: number): Observable<PaginatedBooksResponse> {
|
||||
return this.http.get<PaginatedBooksResponse>(
|
||||
`${this.bookUrl}?page=${page}&size=10&sortBy=lastReadTime&sortDir=desc`
|
||||
);
|
||||
}
|
||||
|
||||
searchBooks(query: string): Observable<Book[]> {
|
||||
if (query.length < 3) {
|
||||
return new Observable<Book[]>();
|
||||
@@ -35,6 +42,10 @@ export class BookService {
|
||||
return this.http.put<void>(url, viewerSetting);
|
||||
}
|
||||
|
||||
getBookSetting(bookId: number): Observable<BookSetting> {
|
||||
return this.http.get<BookSetting>(`${this.bookUrl}/${bookId}/viewer-setting`);
|
||||
}
|
||||
|
||||
getBookDataUrl(bookId: number): string {
|
||||
return `${this.bookUrl}/${bookId}/data`;
|
||||
}
|
||||
@@ -42,4 +53,8 @@ export class BookService {
|
||||
getBookCoverUrl(bookId: number): string {
|
||||
return `${this.bookUrl}/${bookId}/cover`;
|
||||
}
|
||||
|
||||
updateLastReadTime(bookId: number): Observable<void> {
|
||||
return this.http.put<void>(`${this.bookUrl}/${bookId}/update-last-read`, {});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user