Skip to content
2 changes: 1 addition & 1 deletion assets/foliate-js/dist/bundle.js

Large diffs are not rendered by default.

172 changes: 161 additions & 11 deletions assets/foliate-js/src/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,72 @@ const handleSelection = (view, doc, index) => {
});
};

const AUTO_PAGE_DELAY_MS = 1000;
const AUTO_PAGE_SCREEN_BOTTOM_THRESHOLD = 0.9;
const AUTO_PAGE_POST_NEXT_RECHECK_INTERVAL_MS = 120;
const AUTO_PAGE_POST_NEXT_RECHECK_MAX_ATTEMPTS = 12;
const AUTO_PAGE_POST_NEXT_RECHECK_INITIAL_DELAY_MS = 80;

const getAutoPageState = (view) => {
if (!view.__anxAutoPageState) {
view.__anxAutoPageState = {
sessionId: 0,
currentPageKey: null,
triggeredPages: new Set(),
pendingFromPageKey: null,
pendingTimer: null,
postNextRecheckTimer: null,
postNextRecheckAttempts: 0,
awaitingPageAdvanceFromKey: null,
};
}
return view.__anxAutoPageState;
};

const clearAutoPageTimer = (state) => {
if (!state.pendingTimer) return;
clearTimeout(state.pendingTimer);
state.pendingTimer = null;
};

const clearAutoPagePostNextRecheck = (state) => {
if (!state.postNextRecheckTimer) return;
clearTimeout(state.postNextRecheckTimer);
state.postNextRecheckTimer = null;
};

const resetAutoPageSessionState = (state) => {
state.currentPageKey = null;
state.triggeredPages.clear();
state.pendingFromPageKey = null;
state.postNextRecheckAttempts = 0;
state.awaitingPageAdvanceFromKey = null;
};

const startAutoPageSession = (view) => {
const state = getAutoPageState(view);
clearAutoPageTimer(state);
clearAutoPagePostNextRecheck(state);
state.sessionId += 1;
resetAutoPageSessionState(state);
return state;
};

const stopAutoPageSession = (view) => {
const state = getAutoPageState(view);
clearAutoPageTimer(state);
clearAutoPagePostNextRecheck(state);
resetAutoPageSessionState(state);
};

const getAutoPageLocationKey = (lastLocation, index) => {
if (!lastLocation) return `index:${index}:none`;
const cfi = lastLocation.cfi ?? '';
const current = lastLocation.location?.current ?? '';
const chapter = lastLocation.chapterLocation?.current ?? '';
return `index:${index}|cfi:${cfi}|loc:${current}|chapter:${chapter}`;
};

const setSelectionHandler = (view, doc, index) => {
let hasActiveSelection = false;
let lastPointerUpRange = null;
Expand All @@ -192,6 +258,7 @@ const setSelectionHandler = (view, doc, index) => {
lastPointerUpRange = null;
doc.__anxSelectionClearedAt = Date.now();
doc.__anxSuppressClick = true;
stopAutoPageSession(view);
callFlutter('onSelectionCleared');
};

Expand Down Expand Up @@ -346,6 +413,7 @@ const setSelectionHandler = (view, doc, index) => {
const container = view.shadowRoot.querySelector('foliate-paginator').shadowRoot.querySelector("#container");
if (!container) return;
globalThis.originalScrollLeft = container.scrollLeft;
startAutoPageSession(view);
});


Expand All @@ -356,21 +424,103 @@ const setSelectionHandler = (view, doc, index) => {

const selRange = getSelectionRange(doc.getSelection())
if (!selRange) return
if (!lastLocation.range) return;

if (globalThis.pageDebounceTimer) {
clearTimeout(globalThis.pageDebounceTimer);
globalThis.pageDebounceTimer = null;
const container = view.shadowRoot.querySelector('foliate-paginator').shadowRoot.querySelector("#container");
if (!container) return;

const state = getAutoPageState(view);
if (state.sessionId === 0) {
startAutoPageSession(view);
}

const container = view.shadowRoot.querySelector('foliate-paginator').shadowRoot.querySelector("#container");
const pageKey = getAutoPageLocationKey(lastLocation, index);
if (state.currentPageKey !== pageKey) {
if (
state.awaitingPageAdvanceFromKey
&& state.awaitingPageAdvanceFromKey !== pageKey
) {
state.awaitingPageAdvanceFromKey = null;
state.postNextRecheckAttempts = 0;
clearAutoPagePostNextRecheck(state);
}
state.currentPageKey = pageKey;
}

if (state.pendingFromPageKey && state.pendingFromPageKey !== pageKey) {
clearAutoPageTimer(state);
state.pendingFromPageKey = null;
}

if (selRange.compareBoundaryPoints(Range.END_TO_END, lastLocation.range) >= 0) {
globalThis.pageDebounceTimer = setTimeout(async () => {
await view.next();
globalThis.originalScrollLeft = container.scrollLeft;
globalThis.pageDebounceTimer = null;
}, 1000);
return
let compareToPageEnd;
try {
compareToPageEnd = selRange.compareBoundaryPoints(Range.END_TO_END, lastLocation.range);
} catch {
return;
}
const rangeReachedPageEnd = compareToPageEnd >= 0;
const selectionPos = getPosition(selRange);
const nearScreenBottom = selectionPos.bottom >= AUTO_PAGE_SCREEN_BOTTOM_THRESHOLD;
const reachedCurrentPageBottom = rangeReachedPageEnd && nearScreenBottom;

if (!reachedCurrentPageBottom && state.pendingFromPageKey === pageKey) {
clearAutoPageTimer(state);
state.pendingFromPageKey = null;
}

if (reachedCurrentPageBottom) {
if (state.pendingFromPageKey === pageKey) {
if (state.pendingTimer) return;
state.pendingFromPageKey = null;
}
if (state.triggeredPages.has(pageKey)) return;

state.pendingFromPageKey = pageKey;
clearAutoPagePostNextRecheck(state);
state.awaitingPageAdvanceFromKey = null;
state.postNextRecheckAttempts = 0;
clearAutoPageTimer(state);
const scheduledSessionId = state.sessionId;
state.pendingTimer = setTimeout(async () => {
state.pendingTimer = null;
if (scheduledSessionId !== state.sessionId) return;
state.triggeredPages.add(pageKey);
try {
await view.next();
const latestContainer = view.shadowRoot.querySelector('foliate-paginator').shadowRoot.querySelector("#container");
if (latestContainer) {
globalThis.originalScrollLeft = latestContainer.scrollLeft;
}
} finally {
if (state.pendingFromPageKey === pageKey) {
state.pendingFromPageKey = null;
}
state.awaitingPageAdvanceFromKey = pageKey;
state.postNextRecheckAttempts = 0;
clearAutoPagePostNextRecheck(state);
const runPostNextSelectionRecheck = () => {
state.postNextRecheckTimer = null;
if (scheduledSessionId !== state.sessionId) return;
if (state.awaitingPageAdvanceFromKey !== pageKey) return;
state.postNextRecheckAttempts += 1;
try {
doc.dispatchEvent(new Event('selectionchange'));
} catch {
return;
}
if (state.postNextRecheckAttempts >= AUTO_PAGE_POST_NEXT_RECHECK_MAX_ATTEMPTS) return;
state.postNextRecheckTimer = setTimeout(
runPostNextSelectionRecheck,
AUTO_PAGE_POST_NEXT_RECHECK_INTERVAL_MS,
);
};
state.postNextRecheckTimer = setTimeout(
runPostNextSelectionRecheck,
AUTO_PAGE_POST_NEXT_RECHECK_INITIAL_DELAY_MS,
);
}
}, AUTO_PAGE_DELAY_MS);
return;
}

const preventScroll = () => {
Expand Down