diff --git a/backend/src/main/java/com/storycove/controller/StoryController.java b/backend/src/main/java/com/storycove/controller/StoryController.java index b7ce7ce..f0ec16a 100644 --- a/backend/src/main/java/com/storycove/controller/StoryController.java +++ b/backend/src/main/java/com/storycove/controller/StoryController.java @@ -620,11 +620,9 @@ public class StoryController { return 0; } - // Determine the total content length + // ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking) int totalLength = 0; - if (story.getContentPlain() != null && !story.getContentPlain().isEmpty()) { - totalLength = story.getContentPlain().length(); - } else if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) { + if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) { totalLength = story.getContentHtml().length(); } @@ -633,7 +631,8 @@ public class StoryController { } // Calculate percentage and round to nearest integer - return Math.round((float) story.getReadingPosition() * 100 / totalLength); + int percentage = Math.round((float) story.getReadingPosition() * 100 / totalLength); + return Math.min(100, percentage); } private StoryReadingDto convertToReadingDto(Story story) { diff --git a/backend/src/main/java/com/storycove/dto/StorySearchDto.java b/backend/src/main/java/com/storycove/dto/StorySearchDto.java index 3bf2e75..ee8adb8 100644 --- a/backend/src/main/java/com/storycove/dto/StorySearchDto.java +++ b/backend/src/main/java/com/storycove/dto/StorySearchDto.java @@ -18,6 +18,7 @@ public class StorySearchDto { // Reading status private Boolean isRead; private Integer readingPosition; + private Integer readingProgressPercentage; // Pre-calculated percentage (0-100) private LocalDateTime lastReadAt; // Author info @@ -132,7 +133,15 @@ public class StorySearchDto { public void setReadingPosition(Integer readingPosition) { this.readingPosition = readingPosition; } - + + public Integer getReadingProgressPercentage() { + return readingProgressPercentage; + } + + public void setReadingProgressPercentage(Integer readingProgressPercentage) { + this.readingProgressPercentage = readingProgressPercentage; + } + public UUID getAuthorId() { return authorId; } diff --git a/backend/src/main/java/com/storycove/service/SolrService.java b/backend/src/main/java/com/storycove/service/SolrService.java index 444319e..c6c91ed 100644 --- a/backend/src/main/java/com/storycove/service/SolrService.java +++ b/backend/src/main/java/com/storycove/service/SolrService.java @@ -347,6 +347,7 @@ public class SolrService { doc.addField("volume", story.getVolume()); doc.addField("isRead", story.getIsRead()); doc.addField("readingPosition", story.getReadingPosition()); + doc.addField("readingProgressPercentage", calculateReadingProgressPercentage(story)); if (story.getLastReadAt() != null) { doc.addField("lastReadAt", formatDateTime(story.getLastReadAt())); @@ -544,6 +545,26 @@ public class SolrService { return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + "Z"; } + private Integer calculateReadingProgressPercentage(Story story) { + if (story.getReadingPosition() == null || story.getReadingPosition() == 0) { + return 0; + } + + // ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking) + int totalLength = 0; + if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) { + totalLength = story.getContentHtml().length(); + } + + if (totalLength == 0) { + return 0; + } + + // Calculate percentage and round to nearest integer + int percentage = Math.round((float) story.getReadingPosition() * 100 / totalLength); + return Math.min(100, percentage); + } + // =============================== // UTILITY METHODS // =============================== @@ -1039,6 +1060,7 @@ public class SolrService { story.setVolume((Integer) doc.getFieldValue("volume")); story.setIsRead((Boolean) doc.getFieldValue("isRead")); story.setReadingPosition((Integer) doc.getFieldValue("readingPosition")); + story.setReadingProgressPercentage((Integer) doc.getFieldValue("readingProgressPercentage")); // Handle dates story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt"))); diff --git a/frontend/src/app/stories/[id]/page.tsx b/frontend/src/app/stories/[id]/page.tsx index e24ced0..e35f11c 100644 --- a/frontend/src/app/stories/[id]/page.tsx +++ b/frontend/src/app/stories/[id]/page.tsx @@ -95,20 +95,20 @@ export default function StoryReadingPage() { // Convert scroll position to approximate character position in the content const getCharacterPositionFromScroll = useCallback((): number => { if (!contentRef.current || !story) return 0; - + const content = contentRef.current; const scrolled = window.scrollY; const contentTop = content.offsetTop; const contentHeight = content.scrollHeight; const windowHeight = window.innerHeight; - + // Calculate how far through the content we are (0-1) - const scrollRatio = Math.min(1, Math.max(0, + const scrollRatio = Math.min(1, Math.max(0, (scrolled - contentTop + windowHeight * 0.3) / contentHeight )); - - // Convert to character position in the plain text content - const textLength = story.contentPlain?.length || story.contentHtml?.length || 0; + + // Convert to character position in the HTML content (ALWAYS use contentHtml for consistency) + const textLength = story.contentHtml?.length || 0; return Math.floor(scrollRatio * textLength); }, [story]); @@ -116,7 +116,8 @@ export default function StoryReadingPage() { const calculateReadingPercentage = useCallback((currentPosition: number): number => { if (!story) return 0; - const totalLength = story.contentPlain?.length || story.contentHtml?.length || 0; + // ALWAYS use contentHtml for consistency with position calculation + const totalLength = story.contentHtml?.length || 0; if (totalLength === 0) return 0; return Math.round((currentPosition / totalLength) * 100); @@ -126,7 +127,8 @@ export default function StoryReadingPage() { const scrollToCharacterPosition = useCallback((position: number) => { if (!contentRef.current || !story || hasScrolledToPosition) return; - const textLength = story.contentPlain?.length || story.contentHtml?.length || 0; + // ALWAYS use contentHtml for consistency with position calculation + const textLength = story.contentHtml?.length || 0; if (textLength === 0 || position === 0) return; const ratio = position / textLength; diff --git a/solr/stories/conf/managed-schema b/solr/stories/conf/managed-schema index ca2c867..445e227 100755 --- a/solr/stories/conf/managed-schema +++ b/solr/stories/conf/managed-schema @@ -86,6 +86,7 @@ +