Fixes, new sort option "Last Completed"
This commit is contained in:
@@ -604,21 +604,22 @@ public class StoryController {
|
|||||||
dto.setReadingPosition(story.getReadingPosition());
|
dto.setReadingPosition(story.getReadingPosition());
|
||||||
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
||||||
dto.setLastReadAt(story.getLastReadAt());
|
dto.setLastReadAt(story.getLastReadAt());
|
||||||
|
dto.setLastCompletedAt(story.getLastCompletedAt());
|
||||||
|
|
||||||
if (story.getAuthor() != null) {
|
if (story.getAuthor() != null) {
|
||||||
dto.setAuthorId(story.getAuthor().getId());
|
dto.setAuthorId(story.getAuthor().getId());
|
||||||
dto.setAuthorName(story.getAuthor().getName());
|
dto.setAuthorName(story.getAuthor().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (story.getSeries() != null) {
|
if (story.getSeries() != null) {
|
||||||
dto.setSeriesId(story.getSeries().getId());
|
dto.setSeriesId(story.getSeries().getId());
|
||||||
dto.setSeriesName(story.getSeries().getName());
|
dto.setSeriesName(story.getSeries().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.setTags(story.getTags().stream()
|
dto.setTags(story.getTags().stream()
|
||||||
.map(this::convertTagToDto)
|
.map(this::convertTagToDto)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,24 +663,25 @@ public class StoryController {
|
|||||||
dto.setReadingPosition(story.getReadingPosition());
|
dto.setReadingPosition(story.getReadingPosition());
|
||||||
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
||||||
dto.setLastReadAt(story.getLastReadAt());
|
dto.setLastReadAt(story.getLastReadAt());
|
||||||
|
dto.setLastCompletedAt(story.getLastCompletedAt());
|
||||||
|
|
||||||
if (story.getAuthor() != null) {
|
if (story.getAuthor() != null) {
|
||||||
dto.setAuthorId(story.getAuthor().getId());
|
dto.setAuthorId(story.getAuthor().getId());
|
||||||
dto.setAuthorName(story.getAuthor().getName());
|
dto.setAuthorName(story.getAuthor().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (story.getSeries() != null) {
|
if (story.getSeries() != null) {
|
||||||
dto.setSeriesId(story.getSeries().getId());
|
dto.setSeriesId(story.getSeries().getId());
|
||||||
dto.setSeriesName(story.getSeries().getName());
|
dto.setSeriesName(story.getSeries().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.setTags(story.getTags().stream()
|
dto.setTags(story.getTags().stream()
|
||||||
.map(this::convertTagToDto)
|
.map(this::convertTagToDto)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private StorySummaryDto convertToSummaryDto(Story story) {
|
private StorySummaryDto convertToSummaryDto(Story story) {
|
||||||
StorySummaryDto dto = new StorySummaryDto();
|
StorySummaryDto dto = new StorySummaryDto();
|
||||||
dto.setId(story.getId());
|
dto.setId(story.getId());
|
||||||
@@ -700,17 +702,18 @@ public class StoryController {
|
|||||||
dto.setReadingPosition(story.getReadingPosition());
|
dto.setReadingPosition(story.getReadingPosition());
|
||||||
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
|
||||||
dto.setLastReadAt(story.getLastReadAt());
|
dto.setLastReadAt(story.getLastReadAt());
|
||||||
|
dto.setLastCompletedAt(story.getLastCompletedAt());
|
||||||
|
|
||||||
if (story.getAuthor() != null) {
|
if (story.getAuthor() != null) {
|
||||||
dto.setAuthorId(story.getAuthor().getId());
|
dto.setAuthorId(story.getAuthor().getId());
|
||||||
dto.setAuthorName(story.getAuthor().getName());
|
dto.setAuthorName(story.getAuthor().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (story.getSeries() != null) {
|
if (story.getSeries() != null) {
|
||||||
dto.setSeriesId(story.getSeries().getId());
|
dto.setSeriesId(story.getSeries().getId());
|
||||||
dto.setSeriesName(story.getSeries().getName());
|
dto.setSeriesName(story.getSeries().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.setTags(story.getTags().stream()
|
dto.setTags(story.getTags().stream()
|
||||||
.map(this::convertTagToDto)
|
.map(this::convertTagToDto)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class StoryDto {
|
|||||||
private Integer readingPosition;
|
private Integer readingPosition;
|
||||||
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
||||||
private LocalDateTime lastReadAt;
|
private LocalDateTime lastReadAt;
|
||||||
|
private LocalDateTime lastCompletedAt;
|
||||||
|
|
||||||
// Related entities as simple references
|
// Related entities as simple references
|
||||||
private UUID authorId;
|
private UUID authorId;
|
||||||
private String authorName;
|
private String authorName;
|
||||||
@@ -163,7 +164,15 @@ public class StoryDto {
|
|||||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||||
this.lastReadAt = lastReadAt;
|
this.lastReadAt = lastReadAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastCompletedAt() {
|
||||||
|
return lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
|
||||||
|
this.lastCompletedAt = lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public UUID getAuthorId() {
|
public UUID getAuthorId() {
|
||||||
return authorId;
|
return authorId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ public class StoryReadingDto {
|
|||||||
private Integer readingPosition;
|
private Integer readingPosition;
|
||||||
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
||||||
private LocalDateTime lastReadAt;
|
private LocalDateTime lastReadAt;
|
||||||
|
private LocalDateTime lastCompletedAt;
|
||||||
|
|
||||||
// Related entities as simple references
|
// Related entities as simple references
|
||||||
private UUID authorId;
|
private UUID authorId;
|
||||||
private String authorName;
|
private String authorName;
|
||||||
@@ -152,7 +153,15 @@ public class StoryReadingDto {
|
|||||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||||
this.lastReadAt = lastReadAt;
|
this.lastReadAt = lastReadAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastCompletedAt() {
|
||||||
|
return lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
|
||||||
|
this.lastCompletedAt = lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public UUID getAuthorId() {
|
public UUID getAuthorId() {
|
||||||
return authorId;
|
return authorId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public class StorySearchDto {
|
|||||||
private Integer readingPosition;
|
private Integer readingPosition;
|
||||||
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
||||||
private LocalDateTime lastReadAt;
|
private LocalDateTime lastReadAt;
|
||||||
|
private LocalDateTime lastCompletedAt;
|
||||||
|
|
||||||
// Author info
|
// Author info
|
||||||
private UUID authorId;
|
private UUID authorId;
|
||||||
private String authorName;
|
private String authorName;
|
||||||
@@ -126,6 +127,14 @@ public class StorySearchDto {
|
|||||||
this.lastReadAt = lastReadAt;
|
this.lastReadAt = lastReadAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastCompletedAt() {
|
||||||
|
return lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
|
||||||
|
this.lastCompletedAt = lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getReadingPosition() {
|
public Integer getReadingPosition() {
|
||||||
return readingPosition;
|
return readingPosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class StorySummaryDto {
|
|||||||
private Integer readingPosition;
|
private Integer readingPosition;
|
||||||
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
|
||||||
private LocalDateTime lastReadAt;
|
private LocalDateTime lastReadAt;
|
||||||
|
private LocalDateTime lastCompletedAt;
|
||||||
|
|
||||||
// Related entities as simple references
|
// Related entities as simple references
|
||||||
private UUID authorId;
|
private UUID authorId;
|
||||||
@@ -143,7 +144,15 @@ public class StorySummaryDto {
|
|||||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||||
this.lastReadAt = lastReadAt;
|
this.lastReadAt = lastReadAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastCompletedAt() {
|
||||||
|
return lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
|
||||||
|
this.lastCompletedAt = lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public UUID getAuthorId() {
|
public UUID getAuthorId() {
|
||||||
return authorId;
|
return authorId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ public class Story {
|
|||||||
|
|
||||||
@Column(name = "last_read_at")
|
@Column(name = "last_read_at")
|
||||||
private LocalDateTime lastReadAt;
|
private LocalDateTime lastReadAt;
|
||||||
|
|
||||||
|
@Column(name = "last_completed_at")
|
||||||
|
private LocalDateTime lastCompletedAt;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "author_id")
|
@JoinColumn(name = "author_id")
|
||||||
@JsonBackReference("author-stories")
|
@JsonBackReference("author-stories")
|
||||||
@@ -244,6 +247,14 @@ public class Story {
|
|||||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||||
this.lastReadAt = lastReadAt;
|
this.lastReadAt = lastReadAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastCompletedAt() {
|
||||||
|
return lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastCompletedAt(LocalDateTime lastCompletedAt) {
|
||||||
|
this.lastCompletedAt = lastCompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public Author getAuthor() {
|
public Author getAuthor() {
|
||||||
return author;
|
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
|
* 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) {
|
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;
|
this.readingPosition = position;
|
||||||
// Only update lastReadAt if there's actual reading progress
|
// Only update lastReadAt if there's actual reading progress
|
||||||
// Reset to null when position is 0 or null to remove from "last read" sorting
|
// 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() {
|
public void markAsRead() {
|
||||||
this.isRead = true;
|
this.isRead = true;
|
||||||
this.lastReadAt = LocalDateTime.now();
|
this.lastReadAt = LocalDateTime.now();
|
||||||
|
this.lastCompletedAt = LocalDateTime.now();
|
||||||
// Set reading position to the end of content if available
|
// Set reading position to the end of content if available
|
||||||
// ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking)
|
// ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking)
|
||||||
if (contentHtml != null) {
|
if (contentHtml != null) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
|
|||||||
public class AutomaticBackupService {
|
public class AutomaticBackupService {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AutomaticBackupService.class);
|
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");
|
private static final DateTimeFormatter FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
|
||||||
|
|
||||||
@Value("${storycove.automatic-backup.dir:/app/automatic-backups}")
|
@Value("${storycove.automatic-backup.dir:/app/automatic-backups}")
|
||||||
|
|||||||
@@ -353,6 +353,10 @@ public class SolrService {
|
|||||||
doc.addField("lastReadAt", formatDateTime(story.getLastReadAt()));
|
doc.addField("lastReadAt", formatDateTime(story.getLastReadAt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (story.getLastCompletedAt() != null) {
|
||||||
|
doc.addField("lastCompletedAt", formatDateTime(story.getLastCompletedAt()));
|
||||||
|
}
|
||||||
|
|
||||||
if (story.getAuthor() != null) {
|
if (story.getAuthor() != null) {
|
||||||
doc.addField("authorId", story.getAuthor().getId().toString());
|
doc.addField("authorId", story.getAuthor().getId().toString());
|
||||||
doc.addField("authorName", story.getAuthor().getName());
|
doc.addField("authorName", story.getAuthor().getName());
|
||||||
@@ -1064,6 +1068,7 @@ public class SolrService {
|
|||||||
|
|
||||||
// Handle dates
|
// Handle dates
|
||||||
story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt")));
|
story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt")));
|
||||||
|
story.setLastCompletedAt(parseDateTimeFromSolr(doc.getFieldValue("lastCompletedAt")));
|
||||||
story.setCreatedAt(parseDateTimeFromSolr(doc.getFieldValue("createdAt")));
|
story.setCreatedAt(parseDateTimeFromSolr(doc.getFieldValue("createdAt")));
|
||||||
story.setUpdatedAt(parseDateTimeFromSolr(doc.getFieldValue("updatedAt")));
|
story.setUpdatedAt(parseDateTimeFromSolr(doc.getFieldValue("updatedAt")));
|
||||||
story.setDateAdded(parseDateTimeFromSolr(doc.getFieldValue("dateAdded")));
|
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 Logger log = LoggerFactory.getLogger(ZIPImportService.class);
|
||||||
|
|
||||||
private static final long MAX_ZIP_SIZE = 1024L * 1024 * 1024; // 1GB
|
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
|
private static final long ZIP_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
// Temporary storage for extracted ZIP files (sessionId -> session data)
|
// Temporary storage for extracted ZIP files (sessionId -> session data)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ storycove:
|
|||||||
# Query settings
|
# Query settings
|
||||||
query:
|
query:
|
||||||
default-rows: ${SOLR_DEFAULT_ROWS:10}
|
default-rows: ${SOLR_DEFAULT_ROWS:10}
|
||||||
max-rows: ${SOLR_MAX_ROWS:1000}
|
max-rows: ${SOLR_MAX_ROWS:5000}
|
||||||
default-operator: ${SOLR_DEFAULT_OPERATOR:AND}
|
default-operator: ${SOLR_DEFAULT_OPERATOR:AND}
|
||||||
highlight: ${SOLR_ENABLE_HIGHLIGHT:true}
|
highlight: ${SOLR_ENABLE_HIGHLIGHT:true}
|
||||||
facets: ${SOLR_ENABLE_FACETS:true}
|
facets: ${SOLR_ENABLE_FACETS:true}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ class StoryServiceTest {
|
|||||||
|
|
||||||
assertTrue(result.getIsRead());
|
assertTrue(result.getIsRead());
|
||||||
assertNotNull(result.getLastReadAt());
|
assertNotNull(result.getLastReadAt());
|
||||||
|
assertNotNull(result.getLastCompletedAt());
|
||||||
// When marked as read, position should be set to content length
|
// When marked as read, position should be set to content length
|
||||||
assertTrue(result.getReadingPosition() > 0);
|
assertTrue(result.getReadingPosition() > 0);
|
||||||
verify(storyRepository).findById(testId);
|
verify(storyRepository).findById(testId);
|
||||||
@@ -221,6 +222,40 @@ class StoryServiceTest {
|
|||||||
|
|
||||||
assertNotNull(result.getLastReadAt());
|
assertNotNull(result.getLastReadAt());
|
||||||
assertTrue(result.getLastReadAt().isAfter(beforeUpdate));
|
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);
|
verify(storyRepository).save(testStory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,16 +80,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8983:8983" # Expose Solr Admin UI for development
|
- "8983:8983" # Expose Solr Admin UI for development
|
||||||
environment:
|
environment:
|
||||||
- SOLR_HEAP=512m
|
- SOLR_HEAP=1024m
|
||||||
- SOLR_JAVA_MEM=-Xms256m -Xmx512m
|
- SOLR_JAVA_MEM=-Xms512m -Xmx1024m
|
||||||
volumes:
|
volumes:
|
||||||
- /volume1/docker/storycove/solr:/var/solr
|
- /volume1/docker/storycove/solr:/var/solr
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 2G
|
||||||
reservations:
|
reservations:
|
||||||
memory: 512M
|
memory: 1G
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8983/solr/admin/ping || exit 1"]
|
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';
|
import { useLibraryFilters, clearLibraryFilters } from '../../hooks/useLibraryFilters';
|
||||||
|
|
||||||
type ViewMode = 'grid' | 'list';
|
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() {
|
export default function LibraryContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default function MinimalLayout({
|
|||||||
const getSortDisplayText = () => {
|
const getSortDisplayText = () => {
|
||||||
const sortLabels: Record<string, string> = {
|
const sortLabels: Record<string, string> = {
|
||||||
lastReadAt: 'Last Read',
|
lastReadAt: 'Last Read',
|
||||||
|
lastCompletedAt: 'Last Completed',
|
||||||
createdAt: 'Date Added',
|
createdAt: 'Date Added',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
authorName: 'Author',
|
authorName: 'Author',
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export default function SidebarLayout({
|
|||||||
>
|
>
|
||||||
<option value="lastReadAt_desc">Last Read ↓</option>
|
<option value="lastReadAt_desc">Last Read ↓</option>
|
||||||
<option value="lastReadAt_asc">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_desc">Date Added ↓</option>
|
||||||
<option value="createdAt_asc">Date Added ↑</option>
|
<option value="createdAt_asc">Date Added ↑</option>
|
||||||
<option value="title_asc">Title ↑</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"
|
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="lastReadAt">Last Read</option>
|
||||||
|
<option value="lastCompletedAt">Last Completed</option>
|
||||||
<option value="createdAt">Date Added</option>
|
<option value="createdAt">Date Added</option>
|
||||||
<option value="title">Title</option>
|
<option value="title">Title</option>
|
||||||
<option value="authorName">Author</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_desc">Sort: Last Read ↓</option>
|
||||||
<option value="lastReadAt_asc">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_desc">Sort: Date Added ↓</option>
|
||||||
<option value="createdAt_asc">Sort: Date Added ↑</option>
|
<option value="createdAt_asc">Sort: Date Added ↑</option>
|
||||||
<option value="title_asc">Sort: Title ↑</option>
|
<option value="title_asc">Sort: Title ↑</option>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Story {
|
|||||||
readingPosition?: number;
|
readingPosition?: number;
|
||||||
readingProgressPercentage?: number; // Pre-calculated percentage (0-100) from backend
|
readingProgressPercentage?: number; // Pre-calculated percentage (0-100) from backend
|
||||||
lastReadAt?: string;
|
lastReadAt?: string;
|
||||||
|
lastCompletedAt?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user