diff --git a/pyproject.toml b/pyproject.toml index d99041b..e50748c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"] [project] name = "supernote" -version = "1.2.0" +version = "1.3.0" license = "Apache-2.0" license-files = ["LICENSE"] description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services" diff --git a/supernote/server/services/file.py b/supernote/server/services/file.py index 058312c..08a0eca 100644 --- a/supernote/server/services/file.py +++ b/supernote/server/services/file.py @@ -976,7 +976,9 @@ async def convert_note_to_png(self, user: str, file_id: int) -> list[Conversions ) any_new_pages = True - results.append(ConversionsVO(storage_key=png_storage_key, page_no=i)) + results.append( + ConversionsVO(storage_key=png_storage_key, page_no=i + 1) + ) # Persist the MD5 used for this conversion so future calls can clean up stale images. if any_new_pages or node.last_conversion_md5 != current_md5: diff --git a/supernote/server/static/js/components/FileViewer.js b/supernote/server/static/js/components/FileViewer.js index 64088a2..620a40b 100644 --- a/supernote/server/static/js/components/FileViewer.js +++ b/supernote/server/static/js/components/FileViewer.js @@ -1,4 +1,4 @@ -import { ref, computed, watch, onMounted } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'; +import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'; import { convertNoteToPng, fetchStaleness, reprocessFile, reprocessPage } from '../api/client.js'; import SummaryPanel from './SummaryPanel.js'; @@ -18,6 +18,8 @@ export default { const isLoading = ref(false); const error = ref(null); const showDetails = ref(false); + const activePage = ref(1); + const scrollContainerRef = ref(null); // Staleness state const stalenessData = ref(null); // full response from /staleness @@ -114,6 +116,40 @@ export default { } } + // IntersectionObserver: track which page is most visible in the scroll area + let pageObserver = null; + const pageVisibility = new Map(); // pageNo -> intersectionRatio + + function setupPageObserver() { + if (pageObserver) { pageObserver.disconnect(); pageObserver = null; } + if (!scrollContainerRef.value || !pages.value.length) return; + + pageObserver = new IntersectionObserver((records) => { + for (const record of records) { + const pageNo = parseInt(record.target.dataset.pageNo); + pageVisibility.set(pageNo, record.intersectionRatio); + } + let best = activePage.value, bestRatio = -1; + for (const [pageNo, ratio] of pageVisibility) { + if (ratio > bestRatio) { bestRatio = ratio; best = pageNo; } + } + if (bestRatio > 0) activePage.value = best; + }, { root: scrollContainerRef.value, threshold: [0, 0.25, 0.5, 0.75, 1.0] }); + + for (const page of pages.value) { + const el = scrollContainerRef.value.querySelector(`[data-page-no="${page.pageNo}"]`); + if (el) pageObserver.observe(el); + } + } + + watch(pages, async () => { + pageVisibility.clear(); + await nextTick(); + setupPageObserver(); + }, { flush: 'post' }); + + onUnmounted(() => { if (pageObserver) pageObserver.disconnect(); }); + onMounted(loadPages); watch(() => props.file, loadPages); @@ -122,6 +158,8 @@ export default { isLoading, error, showDetails, + activePage, + scrollContainerRef, stalenessData, staleCount, reprocessingAll, @@ -170,7 +208,7 @@ export default {