Skip to content

Commit b648da1

Browse files
committed
feat: remember document sidebar tabs and add red highlight
1 parent a0d4587 commit b648da1

4 files changed

Lines changed: 29 additions & 11 deletions

File tree

‎electron/recents.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ function storedThumbnail(thumbnail){try{return validateThumbnail(thumbnail);}cat
1414
export function validateReadingView(view){
1515
if(!view||typeof view!=='object'||Array.isArray(view))throw Error('Invalid recent document view.');
1616
const keys=Reflect.ownKeys(view);
17-
if(keys.length!==readingViewFields.length||readingViewFields.some(key=>!Object.hasOwn(view,key)))throw Error('Invalid recent document view.');
17+
if(keys.length!==readingViewFields.length+(Object.hasOwn(view,'sidebarMode')?1:0)||readingViewFields.some(key=>!Object.hasOwn(view,key)))throw Error('Invalid recent document view.');
1818
if(!Number.isInteger(view.page)||view.page<1||view.page>1000000)throw Error('Invalid recent document view.');
1919
if(!Number.isFinite(view.offsetX)||view.offsetX<-16||view.offsetX>16||!Number.isFinite(view.offsetY)||view.offsetY<-16||view.offsetY>16)throw Error('Invalid recent document view.');
2020
if(!Number.isFinite(view.zoom)||view.zoom<.1||view.zoom>4)throw Error('Invalid recent document view.');
2121
if(!['width','height','manual'].includes(view.fit)||!['vertical','horizontal'].includes(view.direction)||![1,2,4].includes(view.columns)||typeof view.sidebar!=='boolean'||typeof view.showTranslations!=='boolean')throw Error('Invalid recent document view.');
22-
return {page:view.page,offsetX:view.offsetX,offsetY:view.offsetY,zoom:view.zoom,fit:view.fit,direction:view.direction,columns:view.columns,sidebar:view.sidebar,showTranslations:view.showTranslations};
22+
if(Object.hasOwn(view,'sidebarMode')&&!['thumbnails','outline','annotations'].includes(view.sidebarMode))throw Error('Invalid recent document sidebar mode.');
23+
return {...(Object.hasOwn(view,'sidebarMode')?{sidebarMode:view.sidebarMode}:{}),page:view.page,offsetX:view.offsetX,offsetY:view.offsetY,zoom:view.zoom,fit:view.fit,direction:view.direction,columns:view.columns,sidebar:view.sidebar,showTranslations:view.showTranslations};
2324
}
2425
function storedReadingView(view){try{return validateReadingView(view);}catch{return undefined;}}
2526
function cloneReadingView(view){return view===undefined?undefined:{...view};}

‎server/document-session.test.mjs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,19 @@ test('session persists multiple documents and positions; closing all restores on
1616
assert.equal(JSON.parse(await readFile(path)).documents.length,0);
1717
}finally{await rm(dir,{recursive:true,force:true});}
1818
});
19+
20+
test('sidebar tabs persist separately for each document and reject unknown modes',async()=>{
21+
const {createRecents}=await import('../electron/recents.mjs');
22+
const dir=await mkdtemp(join(tmpdir(),'reader-sidebar-'));
23+
try{
24+
const path=join(dir,'recents.json'),recents=await createRecents(path);
25+
const [a]=await recents.remember('/tmp/a.pdf');
26+
const [b]=await recents.remember('/tmp/b.pdf');
27+
await recents.setView(a.id,{...view,sidebarMode:'annotations'});
28+
await recents.setView(b.id,{...view,sidebarMode:'outline'});
29+
const restored=(await createRecents(path)).list();
30+
assert.equal(restored.find(item=>item.id===a.id).view.sidebarMode,'annotations');
31+
assert.equal(restored.find(item=>item.id===b.id).view.sidebarMode,'outline');
32+
await assert.rejects(recents.setView(a.id,{...view,sidebarMode:'invalid'}));
33+
}finally{await rm(dir,{recursive:true,force:true});}
34+
});

‎src/App.vue‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,13 @@ const restoringView=ref(false);let currentRecentId=null,readingSaveTimer;
250250
function readingView(){
251251
const el=reader.value,p=pages.value[active.value-1],host=p&&pageEls.get(p.number);if(!el||!host)return null;
252252
const bounds=el.getBoundingClientRect(),box=host.getBoundingClientRect(),style=getComputedStyle(el);
253-
return {page:p.number,offsetX:Math.max(-16,Math.min(16,(bounds.left+parseFloat(style.paddingLeft)-box.left)/box.width)),offsetY:Math.max(-16,Math.min(16,(bounds.top+parseFloat(style.paddingTop)-box.top)/box.height)),zoom:zoom.value,fit:fitMode.value,direction:direction.value,columns:columns.value,sidebar:sidebar.value,showTranslations:showTranslations.value};
253+
return {page:p.number,offsetX:Math.max(-16,Math.min(16,(bounds.left+parseFloat(style.paddingLeft)-box.left)/box.width)),offsetY:Math.max(-16,Math.min(16,(bounds.top+parseFloat(style.paddingTop)-box.top)/box.height)),zoom:zoom.value,fit:fitMode.value,direction:direction.value,columns:columns.value,sidebar:sidebar.value,sidebarMode:sidebarMode.value,showTranslations:showTranslations.value};
254254
}
255255
async function saveReadingView(){clearTimeout(readingSaveTimer);if(restoringView.value||loading.value)return;const view=readingView();if(view)try{if(currentRecentId){const id=currentRecentId,status=translationSnapshot();await window.previewRecents?.setView(id,view);await window.previewRecents?.setTranslationStatus?.(id,status);}await window.previewDocuments?.saveView(view);}catch{error.value=t('error.saveReadingPosition');}}
256256
function scheduleReadingSave(){if(restoringView.value||loading.value||!currentRecentId)return;clearTimeout(readingSaveTimer);readingSaveTimer=setTimeout(saveReadingView,180);}
257257
window.previewSaveReadingView=async()=>{await annotationWrites;await performanceRecorder.finish();await saveReadingView();};
258258
async function restoreReadingView(view){
259+
sidebarMode.value=view?.sidebarMode==='outline'&&documentOutline.value.length?'outline':view?.sidebarMode==='annotations'&&annotations.value.length?'annotations':'thumbnails';
259260
if(view){direction.value=view.direction;columns.value=view.columns;sidebar.value=view.sidebar;showTranslations.value=view.showTranslations;fitMode.value=view.fit;zoom.value=view.zoom;active.value=Math.min(pages.value.length,Math.max(1,view.page));pageEntry.value=active.value;}
260261
mountAroundPage(active.value);await nextTick();
261262
if(documentOpening.value)await new Promise(resolve=>requestAnimationFrame(resolve));
@@ -557,7 +558,7 @@ async function closeDocument(closeStartPage=false){
557558
}finally{await sidebarMotion;capture?.element.remove();if(documentMotionController===controller)documentMotionController=null;documentClosing.value=false;closingDocument=false;}
558559
}
559560
async function closeDocumentNow(){
560-
referenceReturn.value=null;referenceNavigation++;const documentToken=epoch;await annotationWrites;annotations.value=[];showAnnotations.value=true;annotationKey.value='';sidebarMode.value='thumbnails';documentOutline.value=[];selectedAnnotation.value=null;await performanceRecorder.finish();await saveReadingView();await window.previewDocuments?.closed();if(documentToken!==epoch)return;currentRecentId=null;
561+
referenceReturn.value=null;referenceNavigation++;const documentToken=epoch;await annotationWrites;await saveReadingView();annotations.value=[];showAnnotations.value=true;annotationKey.value='';sidebarMode.value='thumbnails';documentOutline.value=[];selectedAnnotation.value=null;await performanceRecorder.finish();await window.previewDocuments?.closed();if(documentToken!==epoch)return;currentRecentId=null;
561562
cancel();const closingToken=epoch;closeSearch();await releaseDocument();if(closingToken!==epoch)return;clearTimeout(timer);clearTimeout(navigatorTimer);cancelAnimationFrame(fitFrame);fitFrame=0;clearTimeout(fitResizeTimer);fitResizing=false;
562563
dismissPopovers();resetBitmaps();
563564
const tasks=new Set([pendingPDFTask,pdf?.loadingTask,...pages.value.map(page=>page.mathDocument?.loadingTask)].filter(Boolean));
@@ -579,16 +580,16 @@ async function importFile(file,ticket,origin){
579580
if(file&&!ticket&&(pages.value.length||loading.value)&&window.previewDocuments?.open){try{await window.previewDocuments.open(file);}catch(e){error.value=e.message;}return;}
580581
if(!file)return;closeSearch();if(testMode&&!['A quieter way to read.pdf','Portrait and landscape.pdf'].includes(file.name)){error.value=t('error.testDocumentsDisabled');return;}if(file.size>50*1024*1024){error.value=t('error.PDFTooLarge');return;}
581582
documentMotionController?.abort();documentOpening.value=!!origin&&!documentMotionReduced();
582-
referenceReturn.value=null;referenceNavigation++;await annotationWrites;annotations.value=[];showAnnotations.value=true;annotationKey.value='';sidebarMode.value='thumbnails';documentOutline.value=[];selectedAnnotation.value=null;await performanceRecorder.start(file.size);const runtimeReady=ensurePDF();await saveReadingView();currentRecentId=null;restoringView.value=true;selectedParagraph.value=null;cancel();const token=epoch;await releaseDocument();if(token!==epoch)return;resetBitmaps();renderMetrics.openedAt=performance.now();renderMetrics.firstPageMs=null;loading.value=true;error.value='';for(const p of pages.value)p.mathDocument?.loadingTask.destroy();pages.value=[];pageEls.clear();canvasEls.clear();thumbEls.clear();
583+
referenceReturn.value=null;referenceNavigation++;await annotationWrites;await saveReadingView();annotations.value=[];showAnnotations.value=true;annotationKey.value='';sidebarMode.value='thumbnails';documentOutline.value=[];selectedAnnotation.value=null;await performanceRecorder.start(file.size);const runtimeReady=ensurePDF();currentRecentId=null;restoringView.value=true;selectedParagraph.value=null;cancel();const token=epoch;await releaseDocument();if(token!==epoch)return;resetBitmaps();renderMetrics.openedAt=performance.now();renderMetrics.firstPageMs=null;loading.value=true;error.value='';for(const p of pages.value)p.mathDocument?.loadingTask.destroy();pages.value=[];pageEls.clear();canvasEls.clear();thumbEls.clear();
583584
try{
584585
bytes=new Uint8Array(await file.arrayBuffer());let imported={annotations:[],nativeRefs:[]};if(window.previewAnnotations?.prepare){imported=await window.previewAnnotations.prepare(bytes);bytes=imported.bytes;}else if(window.previewAnnotations?.clean)bytes=await window.previewAnnotations.clean(bytes);performanceRecorder.mark('fileRead');if(token!==epoch)return;await pdf?.loadingTask.destroy();if(token!==epoch)return;
585586
const registered=await api('/api/documents',{method:'POST',headers:{'Content-Type':'application/pdf'},body:bytes});if(token!==epoch){await api('/api/documents/'+registered.id,{method:'DELETE'});return;}documentId=registered.id;performanceRecorder.mark('upload');
586-
await runtimeReady;const task=getDocument({data:bytes});pendingPDFTask=task;const loaded=await task.promise;if(token!==epoch){void task.destroy();return;}pdf=markRaw(loaded);void loadOutline(pdf,token);performanceRecorder.mark('pdfReady');performanceRecorder.pages(pdf.numPages);bytes=undefined;if(pendingPDFTask===task)pendingPDFTask=undefined;
587+
await runtimeReady;const task=getDocument({data:bytes});pendingPDFTask=task;const loaded=await task.promise;if(token!==epoch){void task.destroy();return;}pdf=markRaw(loaded);const outlineReady=loadOutline(pdf,token);performanceRecorder.mark('pdfReady');performanceRecorder.pages(pdf.numPages);bytes=undefined;if(pendingPDFTask===task)pendingPDFTask=undefined;
587588
showTranslations.value=documentOpenMode.value==='translation';translationDeferred.value=documentOpenMode.value==='manual';title.value=(testMode?'TEST — ':'')+file.name;active.value=1;const list=[];
588589
for(let first=1;first<=pdf.numPages;first+=16){const numbers=Array.from({length:Math.min(16,pdf.numPages-first+1)},(_,i)=>first+i),batch=await Promise.all(numbers.map(n=>pdf.getPage(n)));if(token!==epoch)return;for(let i=0;i<batch.length;i++){const v=batch[i].getViewport({scale:1});list.push(reactive({number:numbers[i],width:v.width,height:v.height,status:'idle',blocks:[],cached:false,translationModel:'',dwell:0}));}}
589590
performanceRecorder.mark('pageGeometry');let saved;
590591
if(window.previewRecents)try{const result=await window.previewRecents.remember(file,ticket);if(token!==epoch)return;recentDocuments.value=result.entries;currentRecentId=result.recentId;saved=result.view;}catch{if(token===epoch)error.value=t('error.saveRecentHistory');}
591-
annotationKey.value=currentRecentId||pdf.fingerprints[0];try{const stored=window.previewAnnotations?.loadState?await window.previewAnnotations.loadState(annotationKey.value):{annotations:window.previewAnnotations?await window.previewAnnotations.load(annotationKey.value):JSON.parse(localStorage.getItem('annotations:'+annotationKey.value)||'[]'),nativeRefs:[]};annotationNativeRefs.value=[...new Set([...stored.nativeRefs,...imported.nativeRefs])];annotations.value=[...stored.annotations,...imported.annotations.filter(a=>!stored.annotations.some(b=>b.id===a.id)&&(!a.nativeRef||!stored.nativeRefs.includes(a.nativeRef)))];}catch(e){error.value='无法读取批注:'+e.message;}if(token!==epoch)return;await recoverHighlightedText(pdf,annotations.value);if(token!==epoch)return;performanceRecorder.mark('recentHistory');pages.value=list;await restoreReadingView(saved&&documentOpenMode.value==='manual'?{...saved,showTranslations:false}:saved);if(showTranslations.value)translationDeferred.value=false;performanceRecorder.mark('restoreView');if(token!==epoch)return;observe();observeThumbnails();await renderPages();if(token!==epoch)return;restoringView.value=false;loading.value=false;saveView();await nextTick();
592+
annotationKey.value=currentRecentId||pdf.fingerprints[0];try{const stored=window.previewAnnotations?.loadState?await window.previewAnnotations.loadState(annotationKey.value):{annotations:window.previewAnnotations?await window.previewAnnotations.load(annotationKey.value):JSON.parse(localStorage.getItem('annotations:'+annotationKey.value)||'[]'),nativeRefs:[]};annotationNativeRefs.value=[...new Set([...stored.nativeRefs,...imported.nativeRefs])];annotations.value=[...stored.annotations,...imported.annotations.filter(a=>!stored.annotations.some(b=>b.id===a.id)&&(!a.nativeRef||!stored.nativeRefs.includes(a.nativeRef)))];}catch(e){error.value='无法读取批注:'+e.message;}if(token!==epoch)return;await recoverHighlightedText(pdf,annotations.value);if(token!==epoch)return;performanceRecorder.mark('recentHistory');await outlineReady;if(token!==epoch)return;pages.value=list;await restoreReadingView(saved&&documentOpenMode.value==='manual'?{...saved,showTranslations:false}:saved);if(showTranslations.value)translationDeferred.value=false;performanceRecorder.mark('restoreView');if(token!==epoch)return;observe();observeThumbnails();await renderPages();if(token!==epoch)return;restoringView.value=false;loading.value=false;saveView();await nextTick();
592593
if(origin&&!documentMotionReduced()){
593594
const host=pageEls.get(active.value),capture=captureDocumentPage(host);
594595
if(capture){const controller=new AbortController();documentMotionController=controller;const visibility=host.style.visibility;host.style.visibility='hidden';
@@ -778,7 +779,7 @@ watch(sidebar,()=>{if(!restoringView.value)nextTick(()=>{observeThumbnails();res
778779
watch(title,value=>{document.title=value;});
779780
watch(zoom,async()=>{if(restoringView.value)return;revealControllers.forEach(c=>c.abort());revealControllers.clear();localStorage.setItem('readerZoom',String(zoom.value));if(document.activeElement!==zoomInput.value)zoomEntry.value=formatPercentValue(zoom.value);const request=zoomRequest;zoomRequest=null;if(request&&!pinching.value&&!fitResizing){await applyRequestedZoom({...request,to:zoom.value});}else if(!pinching.value&&!fitResizing){await nextTick();await renderPages();}});watch(concurrency,pump);watch(automatic,()=>{if(automatic.value)settle();});watch(showTranslations,v=>{if(restoringView.value)return;if(v&&translationDeferred.value){translationDeferred.value=false;settle();}for(const p of pages.value)for(const b of p.blocks)b.translated=v;if(engine.value!=='pdf_inspector'){revealControllers.forEach(c=>c.abort());revealControllers.clear();renderPages(true);}});
780781
function visibility(){updateForeground();if(foreground.value){settle();pumpPages();pump();}}
781-
watch([active,zoom,direction,columns,fitMode,sidebar,showTranslations],scheduleReadingSave);
782+
watch([active,zoom,direction,columns,fitMode,sidebar,sidebarMode,showTranslations],scheduleReadingSave);
782783
onMounted(async()=>{if(window.previewActivity){stopActivity=window.previewActivity.onChange(value=>{activityActive.value=value;visibility();});activityActive.value=await window.previewActivity.current();foreground.value=activityActive.value&&!pageHidden();window.previewActivityActive=foreground.value;}if(window.previewRecents){recentDocuments.value=await window.previewRecents.list();scheduleRecentPreviews();}if(window.previewWindow){stopFullscreen=window.previewWindow.onFullscreen(value=>fullscreen.value=value);if(['win32','linux'].includes(platform)){stopMaximized=window.previewWindow.onMaximized(value=>maximized.value=value);maximized.value=await window.previewWindow.maximized();}fullscreen.value=await window.previewWindow.fullscreen();}reader.value?.addEventListener('wheel',pinchWheel,{passive:false});document.addEventListener('keydown',keyboard);document.addEventListener('pointerdown',outsidePopover);document.addEventListener('focusin',outsidePopover);window.addEventListener('blur',dismissPopovers);stopActions=window.previewActions?.onAction(readerAction);if(window.previewPreferences){const saved=await window.previewPreferences.load();applySavedSettings(saved);stopPreferences=window.previewPreferences.onChange?.(applySavedSettings);applyAppearance(saved);engine.value=saved.engine||'pdf_inspector';direction.value=saved.direction||'vertical';columns.value=saved.columns||1;fitMode.value=saved.fit;translationMode.value=saved.translationMode||'reading';if(saved.fit==='manual'){zoom.value=saved.zoom;zoomEntry.value=formatPercentValue(zoom.value);}}await nextTick();syncScrubInputs();loadingPreferences=false;window.addEventListener('resize',resizeFit);resizeObserver=new ResizeObserver(resizeFit);if(reader.value)resizeObserver.observe(reader.value,{box:'content-box'});if(window.previewAppearance){applyAppearance(await window.previewAppearance.current());stopAppearance=window.previewAppearance.onChange(applyAppearance);}const engineReady=checkEngine().then(()=>settle());const c=await api('/api/config');applyConfig(c);model.value=c.model;if(window.previewDocuments){stopDocuments=window.previewDocuments.onAvailable(receiveDocuments);await receiveDocuments();}document.addEventListener('visibilitychange',visibility);settle();scheduleRecentPreviews();if(testMode){await engineReady;window.previewReady=true;}});
783784
onBeforeUnmount(()=>{clearTimeout(recentPreviewTimer);clearInterval(sessionTimer);documentMotionController?.abort();clearTimeout(immersiveLightsTimer);clearTimeout(copyToastTimer);clearTimeout(annotationToastTimer);closeSearch();cancelResize();performanceRecorder.destroy();stopActivity?.();void releaseDocument();void saveReadingView();clearTimeout(readingSaveTimer);delete window.previewSaveReadingView;++recentPreviewGeneration;stopFullscreen?.();stopMaximized?.();reader.value?.removeEventListener('wheel',pinchWheel);cancelAnimationFrame(pinchFrame);clearTimeout(pinchTimer);document.removeEventListener('keydown',keyboard);document.removeEventListener('pointerdown',outsidePopover);document.removeEventListener('focusin',outsidePopover);window.removeEventListener('blur',dismissPopovers);stopActions?.();resizeObserver?.disconnect();window.removeEventListener('resize',resizeFit);cancelAnimationFrame(fitFrame);clearTimeout(fitResizeTimer);clearTimeout(navigatorTimer);scrollbarTimers.forEach(clearTimeout);scrollbarTimers.clear();clearTimeout(previewTimer);clearTimeout(zoomRenderTimer);scrubbers.forEach(entry=>entry.controller.destroy());scrubbers.clear();finishZoomAnimation();cancelAnimationFrame(viewportFrame);resetBitmaps();stopAppearance?.();stopPreferences?.();stopDocuments?.();cancel();clearTimeout(timer);void Promise.allSettled([pdf?.loadingTask,pendingPDFTask,...pages.value.map(p=>p.mathDocument?.loadingTask)].filter(Boolean).map(task=>task.destroy())).then(()=>pdfWorker?.destroy());document.removeEventListener('visibilitychange',visibility);});
784785
</script>

0 commit comments

Comments
 (0)