Fixes, new sort option "Last Completed"

This commit is contained in:
Stefan Hardegger
2026-06-18 12:53:22 +02:00
parent a2ed2f7b79
commit 7358a8ee4e
18 changed files with 127 additions and 27 deletions

View File

@@ -604,21 +604,22 @@ public class StoryController {
dto.setReadingPosition(story.getReadingPosition());
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
dto.setLastReadAt(story.getLastReadAt());
dto.setLastCompletedAt(story.getLastCompletedAt());
if (story.getAuthor() != null) {
dto.setAuthorId(story.getAuthor().getId());
dto.setAuthorName(story.getAuthor().getName());
}
if (story.getSeries() != null) {
dto.setSeriesId(story.getSeries().getId());
dto.setSeriesName(story.getSeries().getName());
}
dto.setTags(story.getTags().stream()
.map(this::convertTagToDto)
.collect(Collectors.toList()));
return dto;
}
@@ -662,24 +663,25 @@ public class StoryController {
dto.setReadingPosition(story.getReadingPosition());
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
dto.setLastReadAt(story.getLastReadAt());
dto.setLastCompletedAt(story.getLastCompletedAt());
if (story.getAuthor() != null) {
dto.setAuthorId(story.getAuthor().getId());
dto.setAuthorName(story.getAuthor().getName());
}
if (story.getSeries() != null) {
dto.setSeriesId(story.getSeries().getId());
dto.setSeriesName(story.getSeries().getName());
}
dto.setTags(story.getTags().stream()
.map(this::convertTagToDto)
.collect(Collectors.toList()));
return dto;
}
private StorySummaryDto convertToSummaryDto(Story story) {
StorySummaryDto dto = new StorySummaryDto();
dto.setId(story.getId());
@@ -700,17 +702,18 @@ public class StoryController {
dto.setReadingPosition(story.getReadingPosition());
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
dto.setLastReadAt(story.getLastReadAt());
dto.setLastCompletedAt(story.getLastCompletedAt());
if (story.getAuthor() != null) {
dto.setAuthorId(story.getAuthor().getId());
dto.setAuthorName(story.getAuthor().getName());
}
if (story.getSeries() != null) {
dto.setSeriesId(story.getSeries().getId());
dto.setSeriesName(story.getSeries().getName());
}
dto.setTags(story.getTags().stream()
.map(this::convertTagToDto)
.collect(Collectors.toList()));

View File

@@ -33,7 +33,8 @@ public class StoryDto {
private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt;
private LocalDateTime lastCompletedAt;
// Related entities as simple references
private UUID authorId;
private String authorName;
@@ -163,7 +164,15 @@ public class StoryDto {
public void setLastReadAt(LocalDateTime lastReadAt) {
this.lastReadAt = lastReadAt;
}
public LocalDateTime getLastCompletedAt() {
return lastCompletedAt;
}
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
this.lastCompletedAt = lastCompletedAt;
}
public UUID getAuthorId() {
return authorId;
}

View File

@@ -27,7 +27,8 @@ public class StoryReadingDto {
private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt;
private LocalDateTime lastCompletedAt;
// Related entities as simple references
private UUID authorId;
private String authorName;
@@ -152,7 +153,15 @@ public class StoryReadingDto {
public void setLastReadAt(LocalDateTime lastReadAt) {
this.lastReadAt = lastReadAt;
}
public LocalDateTime getLastCompletedAt() {
return lastCompletedAt;
}
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
this.lastCompletedAt = lastCompletedAt;
}
public UUID getAuthorId() {
return authorId;
}

View File

@@ -20,7 +20,8 @@ public class StorySearchDto {
private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt;
private LocalDateTime lastCompletedAt;
// Author info
private UUID authorId;
private String authorName;
@@ -126,6 +127,14 @@ public class StorySearchDto {
this.lastReadAt = lastReadAt;
}
public LocalDateTime getLastCompletedAt() {
return lastCompletedAt;
}
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
this.lastCompletedAt = lastCompletedAt;
}
public Integer getReadingPosition() {
return readingPosition;
}

View File

@@ -25,6 +25,7 @@ public class StorySummaryDto {
private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt;
private LocalDateTime lastCompletedAt;
// Related entities as simple references
private UUID authorId;
@@ -143,7 +144,15 @@ public class StorySummaryDto {
public void setLastReadAt(LocalDateTime lastReadAt) {
this.lastReadAt = lastReadAt;
}
public LocalDateTime getLastCompletedAt() {
return lastCompletedAt;
}
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
this.lastCompletedAt = lastCompletedAt;
}
public UUID getAuthorId() {
return authorId;
}

View File

@@ -63,7 +63,10 @@ public class Story {
@Column(name = "last_read_at")
private LocalDateTime lastReadAt;
@Column(name = "last_completed_at")
private LocalDateTime lastCompletedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
@JsonBackReference("author-stories")
@@ -244,6 +247,14 @@ public class Story {
public void setLastReadAt(LocalDateTime lastReadAt) {
this.lastReadAt = lastReadAt;
}
public LocalDateTime getLastCompletedAt() {
return lastCompletedAt;
}
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
this.lastCompletedAt = lastCompletedAt;
}
public Author getAuthor() {
return author;
@@ -290,6 +301,10 @@ public class Story {
* When position is 0 or null, resets lastReadAt to null so the story won't appear in "last read" sorting
*/
public void updateReadingProgress(Integer position) {
// Capture completion timestamp when resetting progress that was already in progress
if ((position == null || position == 0) && this.readingPosition != null && this.readingPosition > 0) {
this.lastCompletedAt = LocalDateTime.now();
}
this.readingPosition = position;
// Only update lastReadAt if there's actual reading progress
// Reset to null when position is 0 or null to remove from "last read" sorting
@@ -306,6 +321,7 @@ public class Story {
public void markAsRead() {
this.isRead = true;
this.lastReadAt = LocalDateTime.now();
this.lastCompletedAt = LocalDateTime.now();
// Set reading position to the end of content if available
// ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking)
if (contentHtml != null) {

View File

@@ -29,7 +29,7 @@ import java.util.stream.Stream;
public class AutomaticBackupService {
private static final Logger logger = LoggerFactory.getLogger(AutomaticBackupService.class);
private static final int MAX_BACKUPS = 5;
private static final int MAX_BACKUPS = 10;
private static final DateTimeFormatter FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
@Value("${storycove.automatic-backup.dir:/app/automatic-backups}")

View File

@@ -353,6 +353,10 @@ public class SolrService {
doc.addField("lastReadAt", formatDateTime(story.getLastReadAt()));
}
if (story.getLastCompletedAt() != null) {
doc.addField("lastCompletedAt", formatDateTime(story.getLastCompletedAt()));
}
if (story.getAuthor() != null) {
doc.addField("authorId", story.getAuthor().getId().toString());
doc.addField("authorName", story.getAuthor().getName());
@@ -1064,6 +1068,7 @@ public class SolrService {
// Handle dates
story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt")));
story.setLastCompletedAt(parseDateTimeFromSolr(doc.getFieldValue("lastCompletedAt")));
story.setCreatedAt(parseDateTimeFromSolr(doc.getFieldValue("createdAt")));
story.setUpdatedAt(parseDateTimeFromSolr(doc.getFieldValue("updatedAt")));
story.setDateAdded(parseDateTimeFromSolr(doc.getFieldValue("dateAdded")));

View File

@@ -22,7 +22,7 @@ public class ZIPImportService {
private static final Logger log = LoggerFactory.getLogger(ZIPImportService.class);
private static final long MAX_ZIP_SIZE = 1024L * 1024 * 1024; // 1GB
private static final int MAX_FILES_IN_ZIP = 30;
private static final int MAX_FILES_IN_ZIP = 100;
private static final long ZIP_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
// Temporary storage for extracted ZIP files (sessionId -> session data)

View File

@@ -76,7 +76,7 @@ storycove:
# Query settings
query:
default-rows: ${SOLR_DEFAULT_ROWS:10}
max-rows: ${SOLR_MAX_ROWS:1000}
max-rows: ${SOLR_MAX_ROWS:5000}
default-operator: ${SOLR_DEFAULT_OPERATOR:AND}
highlight: ${SOLR_ENABLE_HIGHLIGHT:true}
facets: ${SOLR_ENABLE_FACETS:true}

View File

@@ -141,6 +141,7 @@ class StoryServiceTest {
assertTrue(result.getIsRead());
assertNotNull(result.getLastReadAt());
assertNotNull(result.getLastCompletedAt());
// When marked as read, position should be set to content length
assertTrue(result.getReadingPosition() > 0);
verify(storyRepository).findById(testId);
@@ -221,6 +222,40 @@ class StoryServiceTest {
assertNotNull(result.getLastReadAt());
assertTrue(result.getLastReadAt().isAfter(beforeUpdate));
assertNotNull(result.getLastCompletedAt());
assertTrue(result.getLastCompletedAt().isAfter(beforeUpdate));
verify(storyRepository).save(testStory);
}
@Test
@DisplayName("Should set lastCompletedAt when resetting progress from non-zero position")
void shouldSetLastCompletedAtWhenResettingProgress() {
LocalDateTime beforeUpdate = LocalDateTime.now().minusMinutes(1);
// Give the story some reading progress first
testStory.updateReadingProgress(100);
when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
when(storyRepository.save(any(Story.class))).thenReturn(testStory);
Story result = storyService.updateReadingProgress(testId, 0);
assertEquals(0, result.getReadingPosition());
assertNull(result.getLastReadAt());
assertNotNull(result.getLastCompletedAt());
assertTrue(result.getLastCompletedAt().isAfter(beforeUpdate));
verify(storyRepository).save(testStory);
}
@Test
@DisplayName("Should not set lastCompletedAt when resetting progress that was already zero")
void shouldNotSetLastCompletedAtWhenAlreadyAtZeroProgress() {
// testStory starts at position 0, no prior reading
when(storyRepository.findById(testId)).thenReturn(Optional.of(testStory));
when(storyRepository.save(any(Story.class))).thenReturn(testStory);
Story result = storyService.updateReadingProgress(testId, 0);
assertNull(result.getLastCompletedAt());
verify(storyRepository).save(testStory);
}
}