Display and correct calculation of reading progress of a story

This commit is contained in:
Stefan Hardegger
2025-10-31 08:07:12 +01:00
parent 0e1ed7c92e
commit 715fb4e48a
5 changed files with 47 additions and 14 deletions

View File

@@ -620,11 +620,9 @@ public class StoryController {
return 0; return 0;
} }
// Determine the total content length // ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking)
int totalLength = 0; int totalLength = 0;
if (story.getContentPlain() != null && !story.getContentPlain().isEmpty()) { if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) {
totalLength = story.getContentPlain().length();
} else if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) {
totalLength = story.getContentHtml().length(); totalLength = story.getContentHtml().length();
} }
@@ -633,7 +631,8 @@ public class StoryController {
} }
// Calculate percentage and round to nearest integer // 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) { private StoryReadingDto convertToReadingDto(Story story) {

View File

@@ -18,6 +18,7 @@ public class StorySearchDto {
// Reading status // Reading status
private Boolean isRead; private Boolean isRead;
private Integer readingPosition; private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt; private LocalDateTime lastReadAt;
// Author info // Author info
@@ -132,7 +133,15 @@ public class StorySearchDto {
public void setReadingPosition(Integer readingPosition) { public void setReadingPosition(Integer readingPosition) {
this.readingPosition = readingPosition; this.readingPosition = readingPosition;
} }
public Integer getReadingProgressPercentage() {
return readingProgressPercentage;
}
public void setReadingProgressPercentage(Integer readingProgressPercentage) {
this.readingProgressPercentage = readingProgressPercentage;
}
public UUID getAuthorId() { public UUID getAuthorId() {
return authorId; return authorId;
} }

View File

@@ -347,6 +347,7 @@ public class SolrService {
doc.addField("volume", story.getVolume()); doc.addField("volume", story.getVolume());
doc.addField("isRead", story.getIsRead()); doc.addField("isRead", story.getIsRead());
doc.addField("readingPosition", story.getReadingPosition()); doc.addField("readingPosition", story.getReadingPosition());
doc.addField("readingProgressPercentage", calculateReadingProgressPercentage(story));
if (story.getLastReadAt() != null) { if (story.getLastReadAt() != null) {
doc.addField("lastReadAt", formatDateTime(story.getLastReadAt())); doc.addField("lastReadAt", formatDateTime(story.getLastReadAt()));
@@ -544,6 +545,26 @@ public class SolrService {
return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + "Z"; 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 // UTILITY METHODS
// =============================== // ===============================
@@ -1039,6 +1060,7 @@ public class SolrService {
story.setVolume((Integer) doc.getFieldValue("volume")); story.setVolume((Integer) doc.getFieldValue("volume"));
story.setIsRead((Boolean) doc.getFieldValue("isRead")); story.setIsRead((Boolean) doc.getFieldValue("isRead"));
story.setReadingPosition((Integer) doc.getFieldValue("readingPosition")); story.setReadingPosition((Integer) doc.getFieldValue("readingPosition"));
story.setReadingProgressPercentage((Integer) doc.getFieldValue("readingProgressPercentage"));
// Handle dates // Handle dates
story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt"))); story.setLastReadAt(parseDateTimeFromSolr(doc.getFieldValue("lastReadAt")));

View File

@@ -95,20 +95,20 @@ export default function StoryReadingPage() {
// Convert scroll position to approximate character position in the content // Convert scroll position to approximate character position in the content
const getCharacterPositionFromScroll = useCallback((): number => { const getCharacterPositionFromScroll = useCallback((): number => {
if (!contentRef.current || !story) return 0; if (!contentRef.current || !story) return 0;
const content = contentRef.current; const content = contentRef.current;
const scrolled = window.scrollY; const scrolled = window.scrollY;
const contentTop = content.offsetTop; const contentTop = content.offsetTop;
const contentHeight = content.scrollHeight; const contentHeight = content.scrollHeight;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
// Calculate how far through the content we are (0-1) // 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 (scrolled - contentTop + windowHeight * 0.3) / contentHeight
)); ));
// Convert to character position in the plain text content // Convert to character position in the HTML content (ALWAYS use contentHtml for consistency)
const textLength = story.contentPlain?.length || story.contentHtml?.length || 0; const textLength = story.contentHtml?.length || 0;
return Math.floor(scrollRatio * textLength); return Math.floor(scrollRatio * textLength);
}, [story]); }, [story]);
@@ -116,7 +116,8 @@ export default function StoryReadingPage() {
const calculateReadingPercentage = useCallback((currentPosition: number): number => { const calculateReadingPercentage = useCallback((currentPosition: number): number => {
if (!story) return 0; 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; if (totalLength === 0) return 0;
return Math.round((currentPosition / totalLength) * 100); return Math.round((currentPosition / totalLength) * 100);
@@ -126,7 +127,8 @@ export default function StoryReadingPage() {
const scrollToCharacterPosition = useCallback((position: number) => { const scrollToCharacterPosition = useCallback((position: number) => {
if (!contentRef.current || !story || hasScrolledToPosition) return; 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; if (textLength === 0 || position === 0) return;
const ratio = position / textLength; const ratio = position / textLength;

View File

@@ -86,6 +86,7 @@
<!-- Reading Status Fields --> <!-- Reading Status Fields -->
<field name="isRead" type="boolean" indexed="true" stored="true"/> <field name="isRead" type="boolean" indexed="true" stored="true"/>
<field name="readingPosition" type="pint" indexed="true" stored="true"/> <field name="readingPosition" type="pint" indexed="true" stored="true"/>
<field name="readingProgressPercentage" type="pint" indexed="true" stored="true"/>
<field name="lastReadAt" type="pdate" indexed="true" stored="true"/> <field name="lastReadAt" type="pdate" indexed="true" stored="true"/>
<field name="lastRead" type="pdate" indexed="true" stored="true"/> <field name="lastRead" type="pdate" indexed="true" stored="true"/>