mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Websocket and stuffs
This commit is contained in:
@@ -42,6 +42,8 @@ dependencies {
|
||||
implementation 'com.github.jai-imageio:jai-imageio-jpeg2000:1.3.0'
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.3'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket:3.4.0'
|
||||
|
||||
}
|
||||
|
||||
hibernate {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.adityachandel.booklore.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker("/topic");
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws").setAllowedOrigins("*");
|
||||
System.out.println("WebSocket endpoint registered at /ws");
|
||||
}
|
||||
}
|
||||
@@ -30,15 +30,11 @@ public class BookController {
|
||||
}
|
||||
|
||||
@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) {
|
||||
public ResponseEntity<List<BookDTO>> getBooks(@RequestParam(defaultValue = "lastReadTime") String sortBy, @RequestParam(defaultValue = "desc") String sortDir) {
|
||||
if (!sortBy.equals("lastReadTime") && !sortBy.equals("addedOn")) {
|
||||
return ResponseEntity.badRequest().body(null);
|
||||
}
|
||||
Page<BookDTO> books = booksService.getBooks(page, size, sortBy, sortDir);
|
||||
List<BookDTO> books = booksService.getBooks(sortBy, sortDir);
|
||||
return ResponseEntity.ok(books);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/library")
|
||||
@@ -38,11 +41,6 @@ public class LibraryController {
|
||||
return ResponseEntity.ok(libraryService.createLibrary(request));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{libraryId}/parse")
|
||||
public SseEmitter parseLibrary(@RequestParam(required = false, defaultValue = "false") boolean force, @PathVariable long libraryId) {
|
||||
return libraryService.parseLibraryBooks(libraryId, force);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{libraryId}")
|
||||
public ResponseEntity<?> deleteLibrary(@PathVariable long libraryId) {
|
||||
libraryService.deleteLibrary(libraryId);
|
||||
@@ -60,11 +58,8 @@ public class LibraryController {
|
||||
}
|
||||
|
||||
@GetMapping("/{libraryId}/book")
|
||||
public ResponseEntity<Page<BookDTO>> getBooks(
|
||||
@PathVariable long libraryId,
|
||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
||||
@RequestParam(defaultValue = "25") @Min(1) @Max(100) int size) {
|
||||
Page<BookDTO> books = libraryService.getBooks(libraryId, page, size);
|
||||
public ResponseEntity<List<BookDTO>> getBooks(@PathVariable long libraryId) {
|
||||
List<BookDTO> books = libraryService.getBooks(libraryId);
|
||||
return ResponseEntity.ok(books);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.adityachandel.booklore.controller;
|
||||
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class NotificationController {
|
||||
|
||||
/*@MessageMapping("/send")
|
||||
@SendTo("/topic/messages")
|
||||
public String sendMessage(String message) {
|
||||
System.out.println("Received message: " + message);
|
||||
return "Hello";
|
||||
}*/
|
||||
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
public NotificationController(NotificationService notificationService) {
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
@MessageMapping("/send")
|
||||
public void handleIncomingMessage(Object message) {
|
||||
System.out.println("Received message: " + message);
|
||||
}
|
||||
|
||||
public void sendMessageToTopic(String topic, Object message) {
|
||||
notificationService.sendMessage(topic, message);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificat
|
||||
|
||||
Optional<Book> findByFileName(String fileName);
|
||||
|
||||
Page<Book> findBooksByLibraryId(Long libraryId, Pageable pageable);
|
||||
List<Book> findBooksByLibraryId(Long libraryId);
|
||||
|
||||
Optional<Book> findBookByIdAndLibraryId(long id, long libraryId);
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ public class BooksService {
|
||||
private final AuthorRepository authorRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
|
||||
|
||||
|
||||
public BookDTO getBook(long bookId) {
|
||||
@@ -56,19 +59,20 @@ public class BooksService {
|
||||
return BookTransformer.convertToBookDTO(book);
|
||||
}
|
||||
|
||||
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 = Page.empty();
|
||||
public List<BookDTO> getBooks(String sortBy, String sortDir) {
|
||||
int size = 25;
|
||||
PageRequest pageRequest = PageRequest.of(0, size, Sort.by(Sort.Direction.fromString(sortDir), sortBy));
|
||||
Page<Book> bookPage;
|
||||
if (sortBy.equals("addedOn")) {
|
||||
bookPage = bookRepository.findByAddedOnIsNotNull(pageRequest);
|
||||
} else if (sortBy.equals("lastReadTime")) {
|
||||
bookPage = bookRepository.findByLastReadTimeIsNotNull(pageRequest);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid sortBy parameter");
|
||||
}
|
||||
List<BookDTO> bookDTOs = bookPage.getContent().stream()
|
||||
return bookPage.getContent().stream()
|
||||
.map(BookTransformer::convertToBookDTO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageImpl<>(bookDTOs, pageRequest, bookPage.getTotalElements());
|
||||
}
|
||||
|
||||
public void saveBookViewerSetting(long bookId, BookViewerSettingDTO bookViewerSettingDTO) {
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.adityachandel.booklore.service;
|
||||
|
||||
import com.adityachandel.booklore.model.FileProcessResult;
|
||||
import com.adityachandel.booklore.model.LibraryFile;
|
||||
import com.adityachandel.booklore.model.dto.BookDTO;
|
||||
|
||||
public interface FileProcessor {
|
||||
FileProcessResult processFile(LibraryFile libraryFile, boolean forceProcess);
|
||||
BookDTO processFile(LibraryFile libraryFile, boolean forceProcess);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.adityachandel.booklore.service;
|
||||
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.LibraryFile;
|
||||
import com.adityachandel.booklore.model.dto.BookDTO;
|
||||
import com.adityachandel.booklore.model.entity.Library;
|
||||
import com.adityachandel.booklore.repository.LibraryRepository;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class LibraryProcessingService {
|
||||
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final PdfFileProcessor pdfFileProcessor;
|
||||
|
||||
|
||||
@Transactional
|
||||
public void parseLibraryBooks(long libraryId) throws IOException {
|
||||
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
List<LibraryFile> libraryFiles = getLibraryFiles(library);
|
||||
for (LibraryFile libraryFile : libraryFiles) {
|
||||
log.info("Processing file: {}", libraryFile.getFilePath());
|
||||
BookDTO bookDTO = processLibraryFile(libraryFile);
|
||||
if(bookDTO != null) {
|
||||
notificationService.sendMessage("/topic/books", bookDTO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
protected BookDTO processLibraryFile(LibraryFile libraryFile) {
|
||||
if (libraryFile.getFileType().equalsIgnoreCase("pdf")) {
|
||||
return pdfFileProcessor.processFile(libraryFile, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<LibraryFile> getLibraryFiles(Library library) throws IOException {
|
||||
List<LibraryFile> libraryFiles = new ArrayList<>();
|
||||
for (String libraryPath : library.getPaths()) {
|
||||
libraryFiles.addAll(findLibraryFiles(libraryPath, library));
|
||||
}
|
||||
return libraryFiles;
|
||||
}
|
||||
|
||||
private List<LibraryFile> findLibraryFiles(String directoryPath, Library library) throws IOException {
|
||||
List<LibraryFile> libraryFiles = new ArrayList<>();
|
||||
try (var stream = Files.walk(Path.of(directoryPath))) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter(file -> {
|
||||
String fileName = file.getFileName().toString().toLowerCase();
|
||||
return fileName.endsWith(".pdf") || fileName.endsWith(".epub");
|
||||
})
|
||||
.forEach(file -> {
|
||||
String fileType = file.getFileName().toString().toLowerCase().endsWith(".pdf") ? "PDF" : "EPUB";
|
||||
libraryFiles.add(new LibraryFile(library, file.toAbsolutePath().toString(), fileType));
|
||||
});
|
||||
}
|
||||
return libraryFiles;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
package com.adityachandel.booklore.service;
|
||||
|
||||
import com.adityachandel.booklore.model.FileProcessResult;
|
||||
import com.adityachandel.booklore.model.LibraryFile;
|
||||
import com.adityachandel.booklore.model.ParseLibraryEvent;
|
||||
import com.adityachandel.booklore.model.enums.ParsingStatus;
|
||||
import com.adityachandel.booklore.model.dto.*;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.dto.BookDTO;
|
||||
import com.adityachandel.booklore.model.dto.LibraryDTO;
|
||||
import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest;
|
||||
import com.adityachandel.booklore.model.entity.Book;
|
||||
import com.adityachandel.booklore.model.entity.Library;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.repository.*;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.repository.LibraryRepository;
|
||||
import com.adityachandel.booklore.transformer.BookTransformer;
|
||||
import com.adityachandel.booklore.transformer.LibraryTransformer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -40,16 +27,29 @@ public class LibraryService {
|
||||
|
||||
private LibraryRepository libraryRepository;
|
||||
private BookRepository bookRepository;
|
||||
private PdfFileProcessor pdfFileProcessor;
|
||||
private final Map<Long, ReentrantLock> libraryLocks = new ConcurrentHashMap<>();
|
||||
|
||||
private final LibraryProcessingService libraryProcessingService;
|
||||
|
||||
public LibraryDTO createLibrary(CreateLibraryRequest request) {
|
||||
Library library = Library.builder()
|
||||
.name(request.getName())
|
||||
.paths(request.getPaths())
|
||||
.build();
|
||||
return LibraryTransformer.convertToLibraryDTO(libraryRepository.save(library));
|
||||
library = libraryRepository.save(library);
|
||||
Long libraryId = library.getId();
|
||||
|
||||
Thread.startVirtualThread(() -> {
|
||||
log.info("Running in a virtual thread: {}", Thread.currentThread());
|
||||
try {
|
||||
libraryProcessingService.parseLibraryBooks(libraryId);
|
||||
} catch (InvalidDataAccessApiUsageException e) {
|
||||
log.warn("InvalidDataAccessApiUsageException - Library id: {}", libraryId);
|
||||
} catch (IOException e) {
|
||||
log.error("Error while parsing library books", e);
|
||||
}
|
||||
log.info("Parsing task completed!");
|
||||
});
|
||||
|
||||
return LibraryTransformer.convertToLibraryDTO(library);
|
||||
}
|
||||
|
||||
public LibraryDTO getLibrary(long libraryId) {
|
||||
@@ -74,127 +74,9 @@ public class LibraryService {
|
||||
return BookTransformer.convertToBookDTO(book);
|
||||
}
|
||||
|
||||
public Page<BookDTO> getBooks(long libraryId, int page, int size) {
|
||||
public List<BookDTO> getBooks(long libraryId) {
|
||||
libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
PageRequest pageRequest = PageRequest.of(page, size);
|
||||
Page<Book> bookPage = bookRepository.findBooksByLibraryId(libraryId, pageRequest);
|
||||
return bookPage.map(BookTransformer::convertToBookDTO);
|
||||
}
|
||||
|
||||
|
||||
public SseEmitter parseLibraryBooks(long libraryId, boolean force) {
|
||||
ReentrantLock lock = libraryLocks.computeIfAbsent(libraryId, k -> new ReentrantLock());
|
||||
if (!lock.tryLock()) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
emitter.completeWithError(new IllegalStateException("Library is already being processed"));
|
||||
return emitter;
|
||||
}
|
||||
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
if(library.isInitialProcessed()) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
emitter.complete();
|
||||
return emitter;
|
||||
}
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
|
||||
final AtomicBoolean isCompleted = new AtomicBoolean(false);
|
||||
emitter.onTimeout(() -> {
|
||||
log.info("Emitter timeout reached. Stopping further processing.");
|
||||
emitter.complete();
|
||||
isCompleted.set(true);
|
||||
});
|
||||
emitter.onCompletion(() -> {
|
||||
log.info("Emitter completed. Shutting down executor.");
|
||||
isCompleted.set(true);
|
||||
sseMvcExecutor.shutdown();
|
||||
});
|
||||
sseMvcExecutor.execute(() -> {
|
||||
try {
|
||||
List<LibraryFile> libraryFiles = getLibraryFiles(library);
|
||||
for (LibraryFile libraryFile : libraryFiles) {
|
||||
try {
|
||||
log.info("Processing file: {}", libraryFile.getFilePath());
|
||||
FileProcessResult fileProcessResult = processLibraryFile(libraryFile);
|
||||
ParseLibraryEvent event = createParseLibraryEvent(libraryId, fileProcessResult);
|
||||
if (!isCompleted.get()) {
|
||||
emitter.send(event);
|
||||
}
|
||||
Thread.sleep(500);
|
||||
} catch (AsyncRequestNotUsableException | IllegalStateException e) {
|
||||
log.warn("Client disconnected! Continue processing.");
|
||||
} catch (IOException e) {
|
||||
log.warn("Client disconnected or failed to send response: {}", libraryFile.getFilePath(), e);
|
||||
emitter.completeWithError(e);
|
||||
sseMvcExecutor.shutdown();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing file: {}", libraryFile.getFilePath(), e);
|
||||
emitter.completeWithError(e);
|
||||
sseMvcExecutor.shutdown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
library.setInitialProcessed(true);
|
||||
libraryRepository.save(library);
|
||||
emitter.complete();
|
||||
sseMvcExecutor.shutdown();
|
||||
} catch (Exception ex) {
|
||||
log.error("Error during file processing: ", ex);
|
||||
if (!isCompleted.get()) {
|
||||
emitter.completeWithError(ex);
|
||||
sseMvcExecutor.shutdown();
|
||||
}
|
||||
} finally {
|
||||
log.info("File processing complete. Executor shutting down.");
|
||||
lock.unlock();
|
||||
emitter.complete();
|
||||
sseMvcExecutor.shutdown();
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private ParseLibraryEvent createParseLibraryEvent(long libraryId, FileProcessResult result) {
|
||||
return ParseLibraryEvent.builder()
|
||||
.libraryId(libraryId)
|
||||
.file(result.getLibraryFile().getFilePath())
|
||||
.parsingStatus(result.getParsingStatus())
|
||||
.book(result.getBookDTO())
|
||||
.build();
|
||||
}
|
||||
|
||||
private FileProcessResult processLibraryFile(LibraryFile libraryFile) {
|
||||
if (libraryFile.getFileType().equalsIgnoreCase("pdf")) {
|
||||
return pdfFileProcessor.processFile(libraryFile, false);
|
||||
} else if (libraryFile.getFileType().equalsIgnoreCase("epub")) {
|
||||
// TODO:: To implement
|
||||
return FileProcessResult.builder().parsingStatus(ParsingStatus.FAILED_TO_PARSE_BOOK).libraryFile(libraryFile).build();
|
||||
}
|
||||
// TODO:: To handle
|
||||
return FileProcessResult.builder().parsingStatus(ParsingStatus.FAILED_TO_PARSE_BOOK).libraryFile(libraryFile).build();
|
||||
}
|
||||
|
||||
private List<LibraryFile> getLibraryFiles(Library library) throws IOException {
|
||||
List<LibraryFile> libraryFiles = new ArrayList<>();
|
||||
for (String libraryPath : library.getPaths()) {
|
||||
libraryFiles.addAll(findLibraryFiles(libraryPath, library));
|
||||
}
|
||||
return libraryFiles;
|
||||
}
|
||||
|
||||
private List<LibraryFile> findLibraryFiles(String directoryPath, Library library) throws IOException {
|
||||
List<LibraryFile> libraryFiles = new ArrayList<>();
|
||||
try (var stream = Files.walk(Path.of(directoryPath))) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter(file -> {
|
||||
String fileName = file.getFileName().toString().toLowerCase();
|
||||
return fileName.endsWith(".pdf") || fileName.endsWith(".epub");
|
||||
})
|
||||
.forEach(file -> {
|
||||
String fileType = file.getFileName().toString().toLowerCase().endsWith(".pdf") ? "PDF" : "EPUB";
|
||||
libraryFiles.add(new LibraryFile(library, file.toAbsolutePath().toString(), fileType));
|
||||
});
|
||||
}
|
||||
return libraryFiles;
|
||||
List<Book> books = bookRepository.findBooksByLibraryId(libraryId);
|
||||
return books.stream().map(BookTransformer::convertToBookDTO).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.adityachandel.booklore.service;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class NotificationService {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
public NotificationService(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
public void sendMessage(String topic, Object message) {
|
||||
messagingTemplate.convertAndSend(topic, message);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.adityachandel.booklore.service;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.model.enums.ParsingStatus;
|
||||
import com.adityachandel.booklore.model.FileProcessResult;
|
||||
import com.adityachandel.booklore.model.LibraryFile;
|
||||
import com.adityachandel.booklore.model.dto.BookDTO;
|
||||
import com.adityachandel.booklore.model.entity.Book;
|
||||
import com.adityachandel.booklore.model.entity.BookMetadata;
|
||||
import com.adityachandel.booklore.repository.*;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.transformer.BookTransformer;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
@@ -15,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -22,7 +24,10 @@ import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -34,32 +39,24 @@ public class PdfFileProcessor implements FileProcessor {
|
||||
private BookCreatorService bookCreatorService;
|
||||
private AppProperties appProperties;
|
||||
|
||||
@Transactional
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
@Override
|
||||
public FileProcessResult processFile(LibraryFile libraryFile, boolean forceProcess) {
|
||||
public BookDTO processFile(LibraryFile libraryFile, boolean forceProcess) {
|
||||
File bookFile = new File(libraryFile.getFilePath());
|
||||
String fileName = bookFile.getName();
|
||||
if (!forceProcess) {
|
||||
Optional<Book> bookOptional = bookRepository.findBookByFileNameAndLibraryId(fileName, libraryFile.getLibrary().getId());
|
||||
if (bookOptional.isPresent()) {
|
||||
return FileProcessResult.builder()
|
||||
.libraryFile(libraryFile)
|
||||
.bookDTO(BookTransformer.convertToBookDTO(bookOptional.get()))
|
||||
.parsingStatus(ParsingStatus.EXISTING_BOOK_NO_FORCED_UPDATE)
|
||||
.build();
|
||||
} else {
|
||||
return processNewFile(libraryFile);
|
||||
}
|
||||
return bookOptional.map(BookTransformer::convertToBookDTO).orElseGet(() -> processNewFile(libraryFile));
|
||||
} else {
|
||||
return processNewFile(libraryFile);
|
||||
}
|
||||
}
|
||||
|
||||
private FileProcessResult processNewFile(LibraryFile libraryFile) {
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
protected BookDTO processNewFile(LibraryFile libraryFile) {
|
||||
File bookFile = new File(libraryFile.getFilePath());
|
||||
Book book = bookCreatorService.createShellBook(libraryFile);
|
||||
BookMetadata bookMetadata = book.getMetadata();
|
||||
FileProcessResult fileProcessResult;
|
||||
try (PDDocument document = Loader.loadPDF(bookFile)) {
|
||||
if (document.getDocumentInformation() == null) {
|
||||
log.warn("No document information found");
|
||||
@@ -74,19 +71,12 @@ public class PdfFileProcessor implements FileProcessor {
|
||||
}
|
||||
generateCoverImage(bookFile, new File(appProperties.getPathConfig() + "/thumbs"), document);
|
||||
} catch (Exception e) {
|
||||
log.error("Error while processing file {}", libraryFile.getFilePath(), e);
|
||||
return FileProcessResult.builder()
|
||||
.libraryFile(libraryFile)
|
||||
.parsingStatus(ParsingStatus.FAILED_TO_PARSE_BOOK)
|
||||
.build();
|
||||
log.error("Error while processing file {}, error: {}", libraryFile.getFilePath(), e.getMessage());
|
||||
}
|
||||
bookCreatorService.saveConnections(book);
|
||||
fileProcessResult = FileProcessResult.builder()
|
||||
.bookDTO(BookTransformer.convertToBookDTO(book))
|
||||
.parsingStatus(ParsingStatus.PARSED_NEW_BOOK)
|
||||
.libraryFile(libraryFile)
|
||||
.build();
|
||||
return fileProcessResult;
|
||||
bookRepository.save(book);
|
||||
bookRepository.flush();
|
||||
return BookTransformer.convertToBookDTO(book);
|
||||
}
|
||||
|
||||
private Set<String> getAuthors(PDDocument document) {
|
||||
|
||||
230
booklore-ui/package-lock.json
generated
230
booklore-ui/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "my-library-v3",
|
||||
"name": "booklore",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-library-v3",
|
||||
"name": "booklore",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.0.0",
|
||||
@@ -16,6 +16,10 @@
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
|
||||
"@stomp/rx-stomp": "^2.0.0",
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ngx-extended-pdf-viewer": "^22.0.0",
|
||||
"ngx-infinite-scroll": "^19.0.0",
|
||||
"primeflex": "^3.3.1",
|
||||
@@ -23,6 +27,7 @@
|
||||
"primeng": "^17.18.12",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"ws": "^8.18.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -487,33 +492,6 @@
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/localize": {
|
||||
"version": "19.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.0.1.tgz",
|
||||
"integrity": "sha512-tCfTOkdHj6VhskudgsNKF0SS/e0Le+9kv4tPdSsjo9bFcg806lG5/010+UYhy4MDJZ+vDUGTACrMRj2TcFmHRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"fast-glob": "3.3.2",
|
||||
"yargs": "^17.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"localize-extract": "tools/bundles/src/extract/cli.js",
|
||||
"localize-migrate": "tools/bundles/src/migrate/cli.js",
|
||||
"localize-translate": "tools/bundles/src/translate/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "19.0.1",
|
||||
"@angular/compiler-cli": "19.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "19.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.1.tgz",
|
||||
@@ -2628,6 +2606,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@iharbeck/ngx-virtual-scroller": {
|
||||
"version": "19.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-19.0.1.tgz",
|
||||
"integrity": "sha512-dtn4CpbEY92H9nd1A48WNhsyUgtFBjC83xcsc9VzlSQT/KN2fEx0oBs0Obnn6ZdPanDP/IQdlBgmANmlds/wHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tweenjs/tween.js": "^25.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/checkbox": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.2.tgz",
|
||||
@@ -4841,6 +4831,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stomp/rx-stomp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/rx-stomp/-/rx-stomp-2.0.0.tgz",
|
||||
"integrity": "sha512-3UxTxAA3NWGnwFfIvN8AigJ7BxGXG0u5IK8K12mQ9cCMuaT/MM7xlyZnuV8sDbHiqqLlbwA1wk1fDfUyOTIeug==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"rxjs": "^7.2.0",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stomp/stompjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz",
|
||||
"integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tufjs/canonical-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||
@@ -4891,58 +4898,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dev": true,
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
"@types/babel__generator": "*",
|
||||
"@types/babel__template": "*",
|
||||
"@types/babel__traverse": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__generator": {
|
||||
"version": "7.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
|
||||
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__template": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.1.0",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
|
||||
"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
@@ -7219,6 +7180,28 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
@@ -10633,6 +10616,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ng-lazyload-image": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-9.1.3.tgz",
|
||||
"integrity": "sha512-GlajmzbKhQCvg9pcrASq4fe/MNv9KoifGe6N+xRbseaBrNj2uwU4Vwic041NlmAQFEkpDM1H2EJCAjjmJeF7Hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=11.0.0",
|
||||
"@angular/core": ">=11.0.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-extended-pdf-viewer": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-22.0.0.tgz",
|
||||
@@ -12921,6 +12918,28 @@
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
@@ -12947,6 +12966,16 @@
|
||||
"websocket-driver": "^0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
@@ -14019,11 +14048,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@@ -14825,28 +14858,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-merge": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
|
||||
@@ -15105,10 +15116,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
|
||||
"@stomp/rx-stomp": "^2.0.0",
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
"ngx-extended-pdf-viewer": "^22.0.0",
|
||||
"ngx-infinite-scroll": "^19.0.0",
|
||||
"primeflex": "^3.3.1",
|
||||
@@ -25,6 +29,7 @@
|
||||
"primeng": "^17.18.12",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"ws": "^8.18.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {LibraryService} from './book/service/library.service';
|
||||
import {RxStompService} from './rx-stomp.service';
|
||||
import {Message} from '@stomp/stompjs';
|
||||
import {BookService} from './book/service/book.service';
|
||||
import {Book} from './book/model/book.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -9,9 +13,22 @@ import {LibraryService} from './book/service/library.service';
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
constructor(private libraryService: LibraryService) {}
|
||||
constructor(private libraryService: LibraryService, private bookService: BookService, private rxStompService: RxStompService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.libraryService.initializeLibraries();
|
||||
this.bookService.loadBooksSignal(1);
|
||||
|
||||
|
||||
this.rxStompService.watch('/topic/books').subscribe((message: Message) => {
|
||||
const book: Book = JSON.parse(message.body); // Parse the incoming message to book DTO
|
||||
this.bookService.appendBookToLibrary(book); // Call method to append to libraryBooks
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
const message = `Message generated at ${new Date()}`;
|
||||
this.rxStompService.publish({destination: '/app/send', body: message});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ import {SearchComponent} from './book/component/search/search.component';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {FileUploadComponent} from './book/component/file-upload/file-upload.component';
|
||||
import {DropdownModule} from 'primeng/dropdown';
|
||||
import {rxStompServiceFactory} from './rx-stomp-service-factory';
|
||||
import {RxStompService} from './rx-stomp.service';
|
||||
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
|
||||
import {LazyLoadImageModule} from 'ng-lazyload-image';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -46,8 +50,18 @@ import {DropdownModule} from 'primeng/dropdown';
|
||||
InfiniteScrollDirective,
|
||||
SearchComponent,
|
||||
DropdownModule,
|
||||
VirtualScrollerModule,
|
||||
LazyLoadImageModule
|
||||
],
|
||||
providers: [
|
||||
DialogService,
|
||||
MessageService,
|
||||
{
|
||||
provide: RxStompService,
|
||||
useFactory: rxStompServiceFactory,
|
||||
},
|
||||
],
|
||||
providers: [DialogService, MessageService],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<div>
|
||||
<div class="library-header">
|
||||
<h3 class="library-name">{{ libraryNameSignal() }}</h3>
|
||||
<div class="library-header-right">
|
||||
<p-dropdown
|
||||
[options]="cities"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Select Sorting" />
|
||||
<div class="zoom-controls">
|
||||
<p-button icon="pi pi-minus" [rounded]="true" [outlined]="true" (click)="decreaseSize()" [disabled]="isDecreaseDisabled()" severity="info"></p-button>
|
||||
<p-button icon="pi pi-plus" [rounded]="true" [outlined]="true" (click)="increaseSize()" [disabled]="isIncreaseDisabled()" severity="info"></p-button>
|
||||
<div class="library-header">
|
||||
<h3 class="library-name">{{ libraryNameSignal() }}</h3>
|
||||
<div class="library-header-right">
|
||||
<p-dropdown
|
||||
[options]="cities"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Select Sorting" />
|
||||
<div class="zoom-controls">
|
||||
<p-button icon="pi pi-minus" [rounded]="true" [outlined]="true" (click)="decreaseSize()" [disabled]="isDecreaseDisabled()" severity="info"></p-button>
|
||||
<p-button icon="pi pi-plus" [rounded]="true" [outlined]="true" (click)="increaseSize()" [disabled]="isIncreaseDisabled()" severity="info"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="book-list"
|
||||
infiniteScroll
|
||||
[ngClass]="coverSizeClass"
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="loadBooks()">
|
||||
<div class="book-item" *ngFor="let book of books">
|
||||
<img [src]="coverImageSrc(book.id)" class="book-cover placeholder" alt="Cover of {{ book.metadata.title }}" loading="lazy"/>
|
||||
<div class="book-info">
|
||||
<h4 class="book-title">{{ book.metadata.title }}</h4>
|
||||
<p class="book-authors">{{ getAuthorNames(book) }}</p>
|
||||
<p-button [rounded]="true" icon="pi pi-eye" class="view-btn" (click)="readBook(book.id)"></p-button>
|
||||
<p-button [rounded]="true" icon="pi pi-info" class="read-btn" (click)="openBookInfo(book.id, book.libraryId)"></p-button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="content-container">
|
||||
<div class="card-container">
|
||||
<virtual-scroller class="virtual-scroller" #scroll [items]="booksSignal()">
|
||||
<div class="grid" #container>
|
||||
<div class="virtual-scroller-item" *ngFor="let book of scroll.viewPortItems">
|
||||
<!--<img [lazyLoad]="coverImageSrc(book.id)" class="book-cover placeholder"/>-->
|
||||
<img [src]="coverImageSrc(book.id)" class="book-cover placeholder" alt="Cover of {{ book.metadata.title }}" loading="lazy"/>
|
||||
<div class="book-info">
|
||||
<h4 class="book-title">{{ book.metadata.title }}</h4>
|
||||
<p-button [rounded]="true" icon="pi pi-eye" class="view-btn" (click)="readBook(book.id)"></p-button>
|
||||
<p-button [rounded]="true" icon="pi pi-info" class="read-btn" (click)="openBookInfo(book.id, book.libraryId)"></p-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -23,62 +23,49 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
|
||||
gap: 20px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
.virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.small {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.medium {
|
||||
grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
|
||||
}
|
||||
|
||||
.large {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.extra-large {
|
||||
grid-template-columns: repeat(auto-fill, minmax(165px, 1fr));
|
||||
}
|
||||
|
||||
.book-item {
|
||||
.virtual-scroller-item {
|
||||
background-color: var(--surface-card);
|
||||
height: 240px;
|
||||
width: 150px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1.3rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
transition: filter 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.book-item:hover .book-cover {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.book-info {
|
||||
padding: 7px 7px 5px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
padding: 6px 6px 5px;
|
||||
}
|
||||
|
||||
.book-authors {
|
||||
@@ -89,15 +76,7 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.authors {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.no-authors {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-btn, .read-btn {
|
||||
@@ -110,15 +89,19 @@
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.book-item:hover .view-btn,
|
||||
.book-item:hover .read-btn {
|
||||
.virtual-scroller-item:hover .view-btn,
|
||||
.virtual-scroller-item:hover .read-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.book-item:hover .read-btn {
|
||||
.virtual-scroller-item:hover .read-btn {
|
||||
top: 70%;
|
||||
}
|
||||
|
||||
.infinite-scroll {
|
||||
margin-top: 20px;
|
||||
.virtual-scroller-item:hover .book-cover {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit, signal, computed } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BookProgressService } from '../../service/book-progress-service';
|
||||
import { BookService } from '../../service/book.service';
|
||||
import { Book, BookUpdateEvent } from '../../model/book.model';
|
||||
import { combineLatest, Subscription } from 'rxjs';
|
||||
import { InfiniteScrollDirective } from 'ngx-infinite-scroll';
|
||||
import { Button } from 'primeng/button';
|
||||
import { NgClass, NgForOf } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { LibraryService } from '../../service/library.service';
|
||||
import {Component, computed, OnInit, signal} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {BookService} from '../../service/book.service';
|
||||
import {Book} from '../../model/book.model';
|
||||
import {combineLatest} from 'rxjs';
|
||||
import {Button} from 'primeng/button';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {DropdownModule} from 'primeng/dropdown';
|
||||
import {LibraryService} from '../../service/library.service';
|
||||
import {LazyLoadImageModule} from 'ng-lazyload-image';
|
||||
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-browser-v2',
|
||||
templateUrl: './library-browser.component.html',
|
||||
styleUrls: ['./library-browser.component.scss'],
|
||||
imports: [
|
||||
InfiniteScrollDirective,
|
||||
Button,
|
||||
NgForOf,
|
||||
FormsModule,
|
||||
DropdownModule,
|
||||
NgClass
|
||||
LazyLoadImageModule,
|
||||
VirtualScrollerModule
|
||||
]
|
||||
})
|
||||
export class LibraryBrowserComponent implements OnInit, OnDestroy {
|
||||
books: Book[] = [];
|
||||
private currentPage: number = 0;
|
||||
private progressSubscription?: Subscription;
|
||||
export class LibraryBrowserComponent implements OnInit {
|
||||
coverSizeClass = 'medium';
|
||||
coverSizeClasses = ['small', 'medium', 'large', 'extra-large'];
|
||||
|
||||
@@ -41,81 +38,25 @@ export class LibraryBrowserComponent implements OnInit, OnDestroy {
|
||||
selectedCity: any;
|
||||
cities: any[] | undefined;
|
||||
|
||||
booksSignal = computed(() => {
|
||||
return this.bookService.libraryBooks();
|
||||
});
|
||||
|
||||
constructor(
|
||||
private bookService: BookService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private bookProgressService: BookProgressService,
|
||||
private ngZone: NgZone,
|
||||
private libraryService: LibraryService
|
||||
) {}
|
||||
private libraryService: LibraryService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
combineLatest([this.activatedRoute.paramMap, this.activatedRoute.queryParamMap])
|
||||
.subscribe(([params, queryParams]) => {
|
||||
const libraryId = params.get('libraryId');
|
||||
const watch = queryParams.get('watch');
|
||||
if (libraryId) {
|
||||
const libraryIdNum = +libraryId;
|
||||
if (this.libraryIdSignal() !== libraryIdNum) {
|
||||
this.libraryIdSignal.set(libraryIdNum);
|
||||
this.resetState();
|
||||
this.loadBooks();
|
||||
} else if (this.books.length === 0) {
|
||||
this.loadBooks();
|
||||
}
|
||||
}
|
||||
if (watch && !this.progressSubscription) {
|
||||
this.startListeningForProgress();
|
||||
} else if (!watch && this.progressSubscription) {
|
||||
this.stopListeningForProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startListeningForProgress(): void {
|
||||
this.progressSubscription = this.bookProgressService
|
||||
.connect(this.libraryIdSignal())
|
||||
.subscribe({
|
||||
next: (event: BookUpdateEvent) => this.addBookToCollection(event),
|
||||
error: (error) => console.error('Error receiving progress updates:', error),
|
||||
});
|
||||
}
|
||||
|
||||
stopListeningForProgress(): void {
|
||||
if (this.progressSubscription) {
|
||||
this.progressSubscription.unsubscribe();
|
||||
this.progressSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
addBookToCollection(event: BookUpdateEvent): void {
|
||||
if (event.parsingStatus === 'PARSED_NEW_BOOK') {
|
||||
const newBook: Book = {
|
||||
id: event.book.id,
|
||||
libraryId: event.book.libraryId,
|
||||
metadata: event.book.metadata,
|
||||
};
|
||||
this.ngZone.run(() => {
|
||||
if (!this.books.find((book) => book.id === newBook.id)) {
|
||||
this.books = [newBook, ...this.books];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Status other than: PARSED_NEW_BOOK');
|
||||
}
|
||||
}
|
||||
|
||||
loadBooks(): void {
|
||||
this.bookService.loadBooks(this.libraryIdSignal(), 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);
|
||||
}
|
||||
@@ -159,14 +100,4 @@ export class LibraryBrowserComponent implements OnInit, OnDestroy {
|
||||
return this.coverSizeClass === this.coverSizeClasses[0];
|
||||
}
|
||||
|
||||
resetState(): void {
|
||||
this.books = [];
|
||||
this.currentPage = 0;
|
||||
this.stopListeningForProgress();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopListeningForProgress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,16 +4,6 @@ export interface Book {
|
||||
metadata: BookMetadata
|
||||
}
|
||||
|
||||
export interface PaginatedBooksResponse {
|
||||
content: Book[];
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
size: number;
|
||||
number: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
}
|
||||
|
||||
export interface BookMetadata {
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {Book, BookMetadata, BookSetting, BookWithNeighborsDTO, PaginatedBooksResponse} from '../model/book.model';
|
||||
import {Book, BookMetadata, BookSetting, BookWithNeighborsDTO} from '../model/book.model';
|
||||
import {computed, Injectable, signal} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
import {LibraryApiResponse} from '../model/library.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BookService {
|
||||
private readonly pageSize = 50;
|
||||
private readonly libraryUrl = 'http://localhost:8080/v1/library';
|
||||
private readonly bookUrl = 'http://localhost:8080/v1/book';
|
||||
|
||||
@@ -21,10 +19,40 @@ export class BookService {
|
||||
latestAddedBooks = computed(this.#latestAddedBooks);
|
||||
latestAddedBooksLoaded: boolean = false;
|
||||
|
||||
#libraryBooks = signal<Book[]>([]);
|
||||
libraryBooks = computed(this.#libraryBooks);
|
||||
lastLibraryBooksLoaded: boolean = false;
|
||||
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
appendBookToLibrary(book: Book) {
|
||||
const newBook: Book = this.convertBookDTOToBook(book);
|
||||
this.#libraryBooks.set([newBook, ...this.#libraryBooks()]);
|
||||
}
|
||||
|
||||
private convertBookDTOToBook(book: Book): Book {
|
||||
return {
|
||||
id: book.id,
|
||||
libraryId: book.libraryId,
|
||||
metadata: {
|
||||
thumbnail: book.metadata.thumbnail,
|
||||
title: book.metadata.title,
|
||||
subtitle: book.metadata.subtitle,
|
||||
authors: book.metadata.authors,
|
||||
categories: book.metadata.categories,
|
||||
publisher: book.metadata.publisher,
|
||||
publishedDate: book.metadata.publishedDate,
|
||||
isbn10: book.metadata.isbn10,
|
||||
description: book.metadata.description,
|
||||
pageCount: book.metadata.pageCount,
|
||||
language: book.metadata.language,
|
||||
googleBookId: book.metadata.googleBookId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getBook(bookId: number): Observable<Book> {
|
||||
return this.http.get<Book>(`${this.bookUrl}/${bookId}`);
|
||||
}
|
||||
@@ -33,15 +61,31 @@ export class BookService {
|
||||
return this.http.get<BookWithNeighborsDTO>(`${this.libraryUrl}/${libraryId}/book/${bookId}/withNeighbors`);
|
||||
}
|
||||
|
||||
loadBooks(libraryId: number, page: number): Observable<PaginatedBooksResponse> {
|
||||
return this.http.get<PaginatedBooksResponse>(
|
||||
`${this.libraryUrl}/${libraryId}/book?page=${page}&size=${this.pageSize}`
|
||||
loadBooksSignal(libraryId: number) {
|
||||
this.http.get<Book[]>(`${this.libraryUrl}/${libraryId}/book`).pipe(
|
||||
map(response => response),
|
||||
catchError(error => {
|
||||
console.error('Error loading library books:', error);
|
||||
return of([]);
|
||||
})
|
||||
).subscribe(
|
||||
(books) => {
|
||||
this.#libraryBooks.set([...this.#libraryBooks(), ...books]);
|
||||
this.lastReadBooksLoaded = true;
|
||||
console.log("Loaded library books")
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadBooks(libraryId: number): Observable<Book[]> {
|
||||
return this.http.get<Book[]>(
|
||||
`${this.libraryUrl}/${libraryId}/book`
|
||||
);
|
||||
}
|
||||
|
||||
getLastReadBooks() {
|
||||
this.http.get<PaginatedBooksResponse>(`${this.bookUrl}?page=0&size=25&sortBy=lastReadTime&sortDir=desc`).pipe(
|
||||
map(response => response.content),
|
||||
this.http.get<Book[]>(`${this.bookUrl}?sortBy=lastReadTime&sortDir=desc`).pipe(
|
||||
map(response => response),
|
||||
catchError(error => {
|
||||
console.error('Error loading last read books:', error);
|
||||
return of([]);
|
||||
@@ -56,8 +100,8 @@ export class BookService {
|
||||
}
|
||||
|
||||
getLatestAddedBooks() {
|
||||
this.http.get<PaginatedBooksResponse>(`${this.bookUrl}?page=0&size=25&sortBy=addedOn&sortDir=desc`).pipe(
|
||||
map(response => response.content),
|
||||
this.http.get<Book[]>(`${this.bookUrl}?sortBy=addedOn&sortDir=desc`).pipe(
|
||||
map(response => response),
|
||||
catchError(error => {
|
||||
console.error('Error loading latest added books:', error);
|
||||
return of([]);
|
||||
|
||||
30
booklore-ui/src/app/my-rx-stomp.config.ts
Normal file
30
booklore-ui/src/app/my-rx-stomp.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { RxStompConfig } from '@stomp/rx-stomp';
|
||||
|
||||
export const myRxStompConfig: RxStompConfig = {
|
||||
// Which server?
|
||||
brokerURL: 'ws://localhost:8080/ws',
|
||||
|
||||
// Headers
|
||||
// Typical keys: login, passcode, host
|
||||
connectHeaders: {
|
||||
login: 'guest',
|
||||
passcode: 'guest',
|
||||
},
|
||||
|
||||
// How often to heartbeat?
|
||||
// Interval in milliseconds, set to 0 to disable
|
||||
heartbeatIncoming: 0, // Typical value 0 - disabled
|
||||
heartbeatOutgoing: 20000, // Typical value 20000 - every 20 seconds
|
||||
|
||||
// Wait in milliseconds before attempting auto reconnect
|
||||
// Set to 0 to disable
|
||||
// Typical value 500 (500 milli seconds)
|
||||
reconnectDelay: 200,
|
||||
|
||||
// Will log diagnostics on console
|
||||
// It can be quite verbose, not recommended in production
|
||||
// Skip this key to stop logging to console
|
||||
debug: (msg: string): void => {
|
||||
//console.log(new Date(), msg);
|
||||
},
|
||||
};
|
||||
9
booklore-ui/src/app/rx-stomp-service-factory.ts
Normal file
9
booklore-ui/src/app/rx-stomp-service-factory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { RxStompService } from './rx-stomp.service';
|
||||
import { myRxStompConfig } from './my-rx-stomp.config';
|
||||
|
||||
export function rxStompServiceFactory() {
|
||||
const rxStomp = new RxStompService();
|
||||
rxStomp.configure(myRxStompConfig);
|
||||
rxStomp.activate();
|
||||
return rxStomp;
|
||||
}
|
||||
11
booklore-ui/src/app/rx-stomp.service.ts
Normal file
11
booklore-ui/src/app/rx-stomp.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RxStomp } from '@stomp/rx-stomp';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RxStompService extends RxStomp {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user