Fixes, new sort option "Last Completed"
This commit is contained in:
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -80,16 +80,16 @@ services:
|
||||
ports:
|
||||
- "8983:8983" # Expose Solr Admin UI for development
|
||||
environment:
|
||||
- SOLR_HEAP=512m
|
||||
- SOLR_JAVA_MEM=-Xms256m -Xmx512m
|
||||
- SOLR_HEAP=1024m
|
||||
- SOLR_JAVA_MEM=-Xms512m -Xmx1024m
|
||||
volumes:
|
||||
- /volume1/docker/storycove/solr:/var/solr
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
memory: 2G
|
||||
reservations:
|
||||
memory: 512M
|
||||
memory: 1G
|
||||
stop_grace_period: 30s
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8983/solr/admin/ping || exit 1"]
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useLibraryLayout } from '../../hooks/useLibraryLayout';
|
||||
import { useLibraryFilters, clearLibraryFilters } from '../../hooks/useLibraryFilters';
|
||||
|
||||
type ViewMode = 'grid' | 'list';
|
||||
type SortOption = 'createdAt' | 'title' | 'authorName' | 'rating' | 'wordCount' | 'lastReadAt';
|
||||
type SortOption = 'createdAt' | 'title' | 'authorName' | 'rating' | 'wordCount' | 'lastReadAt' | 'lastCompletedAt';
|
||||
|
||||
export default function LibraryContent() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -67,6 +67,7 @@ export default function MinimalLayout({
|
||||
const getSortDisplayText = () => {
|
||||
const sortLabels: Record<string, string> = {
|
||||
lastReadAt: 'Last Read',
|
||||
lastCompletedAt: 'Last Completed',
|
||||
createdAt: 'Date Added',
|
||||
title: 'Title',
|
||||
authorName: 'Author',
|
||||
|
||||
@@ -124,6 +124,7 @@ export default function SidebarLayout({
|
||||
>
|
||||
<option value="lastReadAt_desc">Last Read ↓</option>
|
||||
<option value="lastReadAt_asc">Last Read ↑</option>
|
||||
<option value="lastCompletedAt_desc">Last Completed ↓</option>
|
||||
<option value="createdAt_desc">Date Added ↓</option>
|
||||
<option value="createdAt_asc">Date Added ↑</option>
|
||||
<option value="title_asc">Title ↑</option>
|
||||
@@ -227,6 +228,7 @@ export default function SidebarLayout({
|
||||
className="flex-1 px-3 py-2 border rounded-lg theme-card border-gray-300 dark:border-gray-600"
|
||||
>
|
||||
<option value="lastReadAt">Last Read</option>
|
||||
<option value="lastCompletedAt">Last Completed</option>
|
||||
<option value="createdAt">Date Added</option>
|
||||
<option value="title">Title</option>
|
||||
<option value="authorName">Author</option>
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function ToolbarLayout({
|
||||
>
|
||||
<option value="lastReadAt_desc">Sort: Last Read ↓</option>
|
||||
<option value="lastReadAt_asc">Sort: Last Read ↑</option>
|
||||
<option value="lastCompletedAt_desc">Sort: Last Completed ↓</option>
|
||||
<option value="createdAt_desc">Sort: Date Added ↓</option>
|
||||
<option value="createdAt_asc">Sort: Date Added ↑</option>
|
||||
<option value="title_asc">Sort: Title ↑</option>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface Story {
|
||||
readingPosition?: number;
|
||||
readingProgressPercentage?: number; // Pre-calculated percentage (0-100) from backend
|
||||
lastReadAt?: string;
|
||||
lastCompletedAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user