Skip to content

Commit d712293

Browse files
committed
feat: add Google searches to highlight context menu
1 parent 41f1076 commit d712293

4 files changed

Lines changed: 11 additions & 4 deletions

File tree

‎electron/annotations-smoke.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function verifyAnnotations(window,recents){
2323
for(let i=0;i<100&&!captured;i++)await pause(20);
2424
assert.ok(captured,'annotation event reaches native Electron Menu');
2525
assert.equal(captured.options.window,window);
26-
assert.deepEqual(captured.menu.items.filter(i=>i.type!=='separator').map(i=>i.label),kind==='comment'?['复制','分享','Hand over to AI','修改','删除']:['复制','分享','Hand over to AI','删除']);
26+
assert.deepEqual(captured.menu.items.filter(i=>i.type!=='separator').map(i=>i.label),kind==='comment'?['复制','分享','Hand over to AI','修改','删除']:['复制','分享','谷歌搜索','谷歌学术搜索','Hand over to AI','删除']);
2727
assert.equal(await run(`!!document.querySelector('.annotation-menu')`),false,'macOS has no custom context menu');
2828
if(action)captured.menu.items.find(i=>i.label===action).click();
2929
captured.options.callback();await pause(100);

‎electron/main.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,20 @@ else {
254254
await dialog.showMessageBox(target,{type:'info',message:value.title,detail:value.detail,buttons:[value.close],defaultId:0,cancelId:0,noLink:true});
255255
});
256256
ipcMain.handle('previewAnnotations:palette',async event=>{trustedWindow(event);return annotations.palette();});
257+
ipcMain.handle('previewAnnotations:search',(event,{provider,text}={})=>{
258+
trustedWindow(event);
259+
if(!['google','scholar'].includes(provider)||typeof text!=='string'||!text.trim())throw Error('Invalid search.');
260+
const base=provider==='scholar'?'https://scholar.google.com/scholar':'https://www.google.com/search';
261+
return shell.openExternal(`${base}?q=${encodeURIComponent(text.trim())}`);
262+
});
257263
ipcMain.handle('previewAnnotations:contextMenu',(event,kind)=>{
258264
const target=trustedWindow(event);
259265
if(process.platform!=='darwin')return null;
260266
if(!['highlight','comment'].includes(kind))throw Error('Invalid annotation kind.');
261267
return new Promise(resolve=>{
262268
let selected=null;
263269
const item=label=>({label,click:()=>{selected=label;}});
264-
const template=[item('复制'),item('分享'),item('Hand over to AI'),{type:'separator'},...(kind==='comment'?[item('修改')]:[]),item('删除')];
270+
const template=[item('复制'),item('分享'),...(kind==='highlight'?[item('谷歌搜索'),item('谷歌学术搜索')]:[]),item('Hand over to AI'),{type:'separator'},...(kind==='comment'?[item('修改')]:[]),item('删除')];
265271
Menu.buildFromTemplate(template).popup({window:target,callback:()=>resolve(selected)});
266272
});
267273
});

‎electron/preload.cjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld('previewDocuments',Object.freeze({
2424
contextBridge.exposeInMainWorld('previewTestMode',process.argv.includes('--preview-test-mode'));
2525

2626
contextBridge.exposeInMainWorld('previewAnnotations',Object.freeze({
27+
search:(provider,text)=>ipcRenderer.invoke('previewAnnotations:search',{provider,text}),
2728
contextMenu:kind=>ipcRenderer.invoke('previewAnnotations:contextMenu',kind),
2829
confirmDelete:kind=>ipcRenderer.invoke('previewAnnotations:confirmDelete',kind),
2930
palette:()=>ipcRenderer.invoke('previewAnnotations:palette'),

‎src/ReadingAnnotations.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ async function openContextMenu(a,event){
136136
}
137137
menu.value={a,x:event.clientX,y:event.clientY};
138138
}
139-
async function action(name,a=menu.value?.a){menu.value=null;if(!a)return;try{if(name==='删除'){if(popupPlatform==='darwin'&&window.previewAnnotations?.confirmDelete){if(await window.previewAnnotations.confirmDelete(a.kind)){const current=props.annotations.find(item=>item.id===a.id);if(current)remove(current);}}else{confirming.value=a;await nextTick();confirmPopup.value?.querySelector('button')?.focus();}return;}if(name==='修改'){await comment(a);return;}const text=[a.text,a.comment].filter(Boolean).join('\n\n');if(name==='分享'&&window.previewAnnotations?.share&&await window.previewAnnotations.share(text))return;if(name==='分享'&&navigator.share){await navigator.share({text});return;}if(name==='Hand over to AI'){if(!window.previewAnnotations?.handover)throw Error('请在桌面客户端中交接到本机 ChatGPT 或 Claude。');const result=await window.previewAnnotations.handover(text);if(!result.cancelled)emit('notice',{text:`已打开 ${result.client},内容已复制,请粘贴并发送`,...pointer.value});return;}if(name==='Paraphrase'){assistController?.abort();const controller=assistController=new AbortController();const result={title:'Paraphrase',text:'',busy:true,error:''};assistance.value=result;try{const response=await fetch('/api/reading-assist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'paraphrase',text:a.text,comment:a.comment}),signal:controller.signal});const data=await response.json();if(!response.ok)throw Error(data.error||'请求失败');result.text=data.text;}catch(e){if(e.name!=='AbortError')result.error=e.message;}finally{result.busy=false;if(!controller.signal.aborted)assistance.value={...result};}return;}await (window.previewClipboard?.writeText||navigator.clipboard.writeText.bind(navigator.clipboard))(text);emit('notice',{text:name==='分享'?'已复制分享内容':'已复制',...pointer.value});}catch(e){if(e.name!=='AbortError')emit('notice',{text:'无法完成操作:'+e.message,...pointer.value});}}
139+
async function action(name,a=menu.value?.a){menu.value=null;if(!a)return;try{if(name==='删除'){if(popupPlatform==='darwin'&&window.previewAnnotations?.confirmDelete){if(await window.previewAnnotations.confirmDelete(a.kind)){const current=props.annotations.find(item=>item.id===a.id);if(current)remove(current);}}else{confirming.value=a;await nextTick();confirmPopup.value?.querySelector('button')?.focus();}return;}if(name==='修改'){await comment(a);return;}const text=[a.text,a.comment].filter(Boolean).join('\n\n');if(name==='谷歌搜索'||name==='谷歌学术搜索'){const provider=name==='谷歌学术搜索'?'scholar':'google';const query=(a.text||'').trim();if(!query)return;if(window.previewAnnotations?.search)await window.previewAnnotations.search(provider,query);else window.open((provider==='scholar'?'https://scholar.google.com/scholar':'https://www.google.com/search')+'?q='+encodeURIComponent(query),'_blank','noopener,noreferrer');return;}if(name==='分享'&&window.previewAnnotations?.share&&await window.previewAnnotations.share(text))return;if(name==='分享'&&navigator.share){await navigator.share({text});return;}if(name==='Hand over to AI'){if(!window.previewAnnotations?.handover)throw Error('请在桌面客户端中交接到本机 ChatGPT 或 Claude。');const result=await window.previewAnnotations.handover(text);if(!result.cancelled)emit('notice',{text:`已打开 ${result.client},内容已复制,请粘贴并发送`,...pointer.value});return;}if(name==='Paraphrase'){assistController?.abort();const controller=assistController=new AbortController();const result={title:'Paraphrase',text:'',busy:true,error:''};assistance.value=result;try{const response=await fetch('/api/reading-assist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'paraphrase',text:a.text,comment:a.comment}),signal:controller.signal});const data=await response.json();if(!response.ok)throw Error(data.error||'请求失败');result.text=data.text;}catch(e){if(e.name!=='AbortError')result.error=e.message;}finally{result.busy=false;if(!controller.signal.aborted)assistance.value={...result};}return;}await (window.previewClipboard?.writeText||navigator.clipboard.writeText.bind(navigator.clipboard))(text);emit('notice',{text:name==='分享'?'已复制分享内容':'已复制',...pointer.value});}catch(e){if(e.name!=='AbortError')emit('notice',{text:'无法完成操作:'+e.message,...pointer.value});}}
140140
onMounted(()=>{textObserver=new MutationObserver(records=>{if(records.some(r=>r.target.closest?.('.reading-text-layer,.reading-paragraphs')))updatePopupLayout();});if(props.host())textObserver.observe(props.host(),{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['style','class']});popupObserver=new ResizeObserver(updatePopupLayout);window.addEventListener('resize',updatePopupLayout);window.addEventListener('scroll',updatePopupLayout,true);void window.previewAnnotations?.palette?.().then(value=>{favorite.value=value.favorite;if(value.deleteHintShown)localStorage.setItem('annotationDeleteHint','1');}).catch(()=>{});document.addEventListener('pointerup',capture);document.addEventListener('pointerdown',outside);document.addEventListener('keydown',key);document.addEventListener('pointermove',observePointer);});
141141
onBeforeUnmount(()=>{textObserver?.disconnect();popupObserver?.disconnect();cancelAnimationFrame(layoutFrame);window.removeEventListener('resize',updatePopupLayout);window.removeEventListener('scroll',updatePopupLayout,true);cancelAnimationFrame(motionFrame);eraseTimers.forEach(clearTimeout);eraseTimers.clear();assistController?.abort();clearTimeout(timer);document.removeEventListener('pointerup',capture);document.removeEventListener('pointerdown',outside);document.removeEventListener('keydown',key);document.removeEventListener('pointermove',observePointer);});
142142
watch([opened,editor,toolbar],async()=>{await nextTick();popupObserver?.disconnect();const popup=editor.value?editorPopup.value:opened.value?readerPopup.value:toolbar.value?toolbarPopup.value:null;if(popup)popupObserver?.observe(popup);updatePopupLayout();},{flush:'post'});
@@ -157,7 +157,7 @@ watch(()=>props.zoom,()=>{toolbar.value=false;updatePopupLayout();});
157157
</div></Transition>
158158
<Transition name="annotation-fade"><div v-if="opened" ref="readerPopup" :data-platform="popupPlatform" class="annotation-ui annotation-reader" :style="readerPoint()"><header class="annotation-reader-header"><span>{{opened.origin==='translation'?'译文':'原文'}}{{opened.kind==='highlight'?'高亮':'批注'}}</span><button @click="opened=null" aria-label="关闭"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m4 4 8 8M12 4l-8 8"/></svg></button></header><p>{{opened.kind==='highlight'?(opened.text||'').replace(/[\r\n\u2028\u2029]+/g,'').trim():opened.comment||opened.text}}</p><time>{{opened.dateUnknown&&!opened.modifiedAt?'日期未知':new Date(opened.modifiedAt||opened.createdAt).toLocaleString()}}</time></div></Transition>
159159
<Transition name="annotation-fade"><div v-if="editor" :data-platform="popupPlatform" class="annotation-ui annotation-modal" @pointerdown.self="editor=null"><form ref="editorPopup" class="annotation-editor" :style="editorPoint()" role="dialog" aria-modal="true" aria-label="添加批注" @submit.prevent="commit"><textarea v-model="draft" placeholder="输入评论…" aria-label="评论"/><div><button type="button" @click="editor=null">关闭</button><button type="submit" :disabled="!draft.trim()">保存 ⌘↵</button></div></form></div></Transition>
160-
<div v-if="menu" :data-platform="popupPlatform" class="annotation-ui annotation-menu" :style="{left:Math.min(menu.x,innerWidth-190)+'px',top:Math.min(menu.y,innerHeight-250)+'px'}" role="menu"><button v-for="name in menu.a.kind==='highlight'?['复制','分享','Hand over to AI','删除']:['复制','分享','Hand over to AI','修改','删除']" :key="name" role="menuitem" @click="action(name)">{{name}}</button></div>
160+
<div v-if="menu" :data-platform="popupPlatform" class="annotation-ui annotation-menu" :style="{left:Math.min(menu.x,innerWidth-190)+'px',top:Math.min(menu.y,innerHeight-330)+'px'}" role="menu"><button v-for="name in menu.a.kind==='highlight'?['复制','分享','谷歌搜索','谷歌学术搜索','Hand over to AI','删除']:['复制','分享','Hand over to AI','修改','删除']" :key="name" role="menuitem" @click="action(name)">{{name}}</button></div>
161161
<div v-if="confirming" :data-platform="popupPlatform" class="annotation-ui annotation-modal"><section ref="confirmPopup" class="annotation-confirm annotation-delete-confirm" role="alertdialog" aria-modal="true" :aria-label="`删除这条${confirming.kind==='highlight'?'高亮':'批注'}?`" @keydown.tab.prevent="confirmPopup.querySelectorAll('button')[($event.target===confirmPopup.querySelector('button'))?1:0].focus()"><p>删除这条{{confirming.kind==='highlight'?'高亮':'批注'}}?</p><div class="annotation-confirm-actions"><button @click="confirming=null">取消</button><button class="annotation-confirm-destructive" @click="remove(confirming)">删除</button></div></section></div>
162162
</Teleport>
163163
</template>

0 commit comments

Comments
 (0)