feat: user-interaction content read
This commit is contained in:
parent
1827e39471
commit
5da02723cb
@ -2,8 +2,8 @@ package com.magamochi.content.controller;
|
|||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.content.model.dto.MangaContentDTO;
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
||||||
import com.magamochi.content.service.ContentService;
|
import com.magamochi.content.service.ContentService;
|
||||||
import com.magamochi.model.dto.MangaContentImagesDTO;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@ -10,12 +10,12 @@ public record MangaContentDTO(
|
|||||||
@NotNull Boolean downloaded,
|
@NotNull Boolean downloaded,
|
||||||
@NotNull Boolean isRead,
|
@NotNull Boolean isRead,
|
||||||
LanguageDTO language) {
|
LanguageDTO language) {
|
||||||
public static MangaContentDTO from(MangaContent mangaContent) {
|
public static MangaContentDTO from(MangaContent mangaContent, boolean isRead) {
|
||||||
return new MangaContentDTO(
|
return new MangaContentDTO(
|
||||||
mangaContent.getId(),
|
mangaContent.getId(),
|
||||||
mangaContent.getTitle(),
|
mangaContent.getTitle(),
|
||||||
mangaContent.getDownloaded(),
|
mangaContent.getDownloaded(),
|
||||||
false,
|
isRead,
|
||||||
LanguageDTO.from(mangaContent.getLanguage()));
|
LanguageDTO.from(mangaContent.getLanguage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
@ -3,9 +3,10 @@ package com.magamochi.content.service;
|
|||||||
import com.magamochi.catalog.service.MangaContentProviderService;
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.content.model.dto.MangaContentDTO;
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.content.model.repository.MangaContentRepository;
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
import com.magamochi.model.dto.MangaContentImagesDTO;
|
import com.magamochi.userinteraction.service.UserMangaContentReadService;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -16,6 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentService {
|
public class ContentService {
|
||||||
private final MangaContentProviderService mangaContentProviderService;
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
private final UserMangaContentReadService userMangaContentReadService;
|
||||||
|
|
||||||
private final MangaContentRepository mangaContentRepository;
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
|
||||||
@ -24,7 +26,11 @@ public class ContentService {
|
|||||||
|
|
||||||
return mangaContentProvider.getMangaContents().stream()
|
return mangaContentProvider.getMangaContents().stream()
|
||||||
.sorted(Comparator.comparing(MangaContent::getId))
|
.sorted(Comparator.comparing(MangaContent::getId))
|
||||||
.map(MangaContentDTO::from)
|
.map(
|
||||||
|
mangaContent -> {
|
||||||
|
var isRead = userMangaContentReadService.isRead(mangaContent.getId());
|
||||||
|
return MangaContentDTO.from(mangaContent, isRead);
|
||||||
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.magamochi.controller;
|
package com.magamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
import com.magamochi.model.enumeration.ArchiveFileType;
|
||||||
import com.magamochi.service.MangaChapterService;
|
import com.magamochi.service.MangaChapterService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@ -20,18 +19,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class MangaChapterController {
|
public class MangaChapterController {
|
||||||
private final MangaChapterService mangaChapterService;
|
private final MangaChapterService mangaChapterService;
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Mark a chapter as read",
|
|
||||||
description = "Mark a chapter as read by its ID.",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "markAsRead")
|
|
||||||
@PostMapping("/{chapterId}/mark-as-read")
|
|
||||||
public DefaultResponseDTO<Void> markAsRead(@PathVariable Long chapterId) {
|
|
||||||
mangaChapterService.markAsRead(chapterId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Download chapter archive",
|
summary = "Download chapter archive",
|
||||||
description = "Download a chapter as a compressed file by its ID.",
|
description = "Download a chapter as a compressed file by its ID.",
|
||||||
|
|||||||
@ -27,14 +27,6 @@ public class MangaChapterService {
|
|||||||
|
|
||||||
private final OldImageService oldImageService;
|
private final OldImageService oldImageService;
|
||||||
|
|
||||||
public void markAsRead(Long chapterId) {
|
|
||||||
// TODO: implement this
|
|
||||||
// var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
// chapter.setRead(true);
|
|
||||||
//
|
|
||||||
// mangaChapterRepository.save(chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.magamochi.userinteraction.controller;
|
|||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.userinteraction.service.UserFavoriteMangaService;
|
import com.magamochi.userinteraction.service.UserFavoriteMangaService;
|
||||||
|
import com.magamochi.userinteraction.service.UserMangaContentReadService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserInteractionController {
|
public class UserInteractionController {
|
||||||
private final UserFavoriteMangaService userFavoriteMangaService;
|
private final UserFavoriteMangaService userFavoriteMangaService;
|
||||||
|
private final UserMangaContentReadService userMangaContentReadService;
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Favorite a manga",
|
summary = "Favorite a manga",
|
||||||
@ -38,4 +40,16 @@ public class UserInteractionController {
|
|||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Mark content as read",
|
||||||
|
description = "Mark content as read by its ID.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "markContentAsRead")
|
||||||
|
@PostMapping("/content/{mangaContentId}/read")
|
||||||
|
public DefaultResponseDTO<Void> markContentAsRead(@PathVariable Long mangaContentId) {
|
||||||
|
userMangaContentReadService.setRead(mangaContentId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.magamochi.userinteraction.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_manga_content_read")
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserMangaContentRead {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "manga_content_id")
|
||||||
|
private MangaContent mangaContent;
|
||||||
|
|
||||||
|
@CreationTimestamp private Instant createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.magamochi.userinteraction.model.repository;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface UserMangaContentReadRepository extends JpaRepository<UserMangaContentRead, Long> {
|
||||||
|
boolean existsByUserAndMangaContent(User user, MangaContent mangaContent);
|
||||||
|
|
||||||
|
Optional<UserMangaContentRead> findByUserAndMangaContent(User user, MangaContent mangaContent);
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.magamochi.userinteraction.service;
|
||||||
|
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
|
import com.magamochi.user.service.UserService;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserMangaContentReadRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserMangaContentReadService {
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
private final UserMangaContentReadRepository userMangaContentReadRepository;
|
||||||
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
|
||||||
|
public void setRead(Long id) {
|
||||||
|
try {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
if (userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userMangaContentReadRepository.save(
|
||||||
|
UserMangaContentRead.builder().user(user).mangaContent(mangaContent).build());
|
||||||
|
} catch (NotFoundException _) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnread(Long id) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
var mangaContentRead =
|
||||||
|
userMangaContentReadRepository.findByUserAndMangaContent(user, mangaContent);
|
||||||
|
|
||||||
|
mangaContentRead.ifPresent(userMangaContentReadRepository::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRead(Long id) {
|
||||||
|
try {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
return userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MangaContent findMangaContent(Long id) {
|
||||||
|
return mangaContentRepository
|
||||||
|
.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id));
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main/resources/db/migration/V0003__CONTENT_READ.sql
Normal file
8
src/main/resources/db/migration/V0003__CONTENT_READ.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE user_manga_content_read
|
||||||
|
(
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
manga_content_id BIGINT NOT NULL REFERENCES manga_contents (id) ON DELETE CASCADE,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, manga_content_id)
|
||||||
|
);
|
||||||
Loading…
x
Reference in New Issue
Block a user