update AI engines
[uweb.git] / misc / ebrowser / webview.js
blob9020686f430ef9c19c6662a7141ce932b173ed16
1 /* Copyright (C) 2024 Richard Hao Cao
2 Ebrowser is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Ebrowser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6 You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7 */
8 const {
9 app, BrowserWindow, Menu, shell, clipboard,
10 session, protocol, dialog, ipcMain
11 } = require('electron')
12 let win;
14 if(!app.requestSingleInstanceLock())
15 app.quit()
16 else {
17 app.on('ready', createWindow);
18 app.on('second-instance', (event, args, cwd) => {
19 if (win) {
20 if (win.isMinimized()) {
21 win.restore()
23 win.show()
24 win.focus()
25 cmdlineProcess(args,cwd,1);
26 }else
27 createWindow();
30 Menu.setApplicationMenu(null);
31 const fs = require('fs');
32 const path = require('path')
33 const https = require('https');
34 const url = require('url');
35 var translateRes;
37 let langs = app.getPreferredSystemLanguages();
38 if(langs.length==0 || langs[0].startsWith('en'))
39 topMenu();
40 else
41 initTranslateRes(langs[0]);
44 var repositoryurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
45 const readline = require('readline');
46 const process = require('process')
47 var gredirects = [];
48 var gredirect;
49 var redirects;
50 var bRedirect = true;
51 var bJS = true;
52 var bForwardCookie = true;
53 var proxies = {};
54 var proxy;
55 var useragents = {};
56 var downloadMenus; //[]
57 var selectMenus = [];
58 var defaultUA;
60 let sys = "X11; Linux x86_64";
61 if (process.platform === "win32")
62 sys = "Window NT 10.0; Win64; x64";
63 else if (process.platform === "darwin")
64 sys = "Macintosh; Intel Mac OS X 10_15_7";
65 defaultUA =
66 `Mozilla/5.0 (${sys}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/` +
67 process.versions.chrome +" Safari/537.36";
69 app.userAgentFallback = defaultUA;
71 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
72 if (err) return;
73 try {
74 redirects = JSON.parse(jsonString);
75 } catch (e){console.log(e)}
76 });
78 async function createWindow () {
79 try {
80 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
81 useragents = JSON.parse(json);
82 } catch (e){console.log(e)}
84 protocol.handle("i",(req)=>{return null;});
85 await (async ()=>{
86 try{
87 const readInterface = readline.createInterface ({
88 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
89 });
91 for await (const line of readInterface) {
92 addrCommand(line);
94 }catch(e){console.log(e);}
95 })();
97 win = new BrowserWindow(
98 {width: 800, height: 600,autoHideMenuBar: true,
99 webPreferences: {
100 nodeIntegration: true,
101 contextIsolation: false,
102 webviewTag: true,
103 }});
104 win.setMenuBarVisibility(false);
105 win.on('closed', function () {
106 win = null
109 win.loadFile('index.html');
110 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
111 if (err) return;
112 try {
113 gredirects = JSON.parse(jsonString);
114 } catch (e){console.log(e)}
117 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
118 if (err) return;
119 try {
120 proxies = JSON.parse(jsonString);
121 let match = jsonString.match(/"([^"]+)"/);
122 if(match)
123 proxy = proxies[match[1]];
124 } catch (e){console.log(e)}
127 cmdlineProcess(process.argv, process.cwd(), 0);
128 //app.commandLine.appendSwitch ('trace-warnings');
130 fs.readFile(path.join(__dirname,'download.json'), 'utf8', (err, jsonStr) => {
131 if (err) return;
132 try {
133 downloadMenus = JSON.parse(jsonStr);
134 }catch (e){console.log(e)}
137 fs.readFile(path.join(__dirname,'select.json'), 'utf8', (err, jsonStr) => {
138 if (err) return;
139 try {
140 selectMenus = JSON.parse(jsonStr);
141 }catch (e){console.log(e)}
144 win.webContents.on('page-title-updated',(event,cmd)=>{
145 addrCommand(cmd);
148 session.defaultSession.on("will-download", async (e, item) => {
149 //item.setSavePath(save)
150 if(!downloadMenus) return;
151 let menuT = downloadContextMenuTemp(item.getURL());
152 let button = await promiseContextMenu(menuT);
153 if(-1===button) return;
154 e.preventDefault();
157 win.webContents.on('console-message',cbConsoleMsg);
160 app.on('window-all-closed', function () {
161 app.quit()
164 app.on('activate', function () {
165 if (win === null) {
166 createWindow()
170 app.on('will-quit', () => {
173 app.on ('web-contents-created', (event, contents) => {
174 if (contents.getType () === 'webview') {
175 contents.setWindowOpenHandler(cbWindowOpenHandler);
176 contents.on('context-menu',onContextMenu);
177 contents.on('page-title-updated',cbTitleUpdate);
178 contents.session.webRequest.onBeforeRequest(interceptRequest);
182 ipcMain.on('command', (event, cmd) => {
183 addrCommand(cmd);
186 async function addrCommand(cmd){
187 if(cmd.length<3) return;
188 let c0 = cmd.charCodeAt(0);
189 switch(c0){
190 case 33://"!"
191 bangcommand(q,1);
192 return;
193 case 58: //':'
194 let iS = cmd.indexOf(' ',1);
195 if(iS<0) iS = cmd.length;
196 let arg0 = cmd.substring(1,iS);
197 switch(arg0){
198 case "cert":
199 if(cmd.length==iS)
200 session.defaultSession.setCertificateVerifyProc((request, callback) => {
201 callback(0);
203 else
204 session.defaultSession.setCertificateVerifyProc(null);
205 return;
206 case "clear":
207 if(cmd.length<=iS+1){
208 session.defaultSession.clearData();
209 return;
211 if(123===cmd.charCodeAt(iS+1)){//json
212 try {
213 let opts = JSON.parse(cmd.substring(iS+1));
214 session.defaultSession.clearData(opts);
215 }catch(e){console.log(e)}
216 return;
218 let args = cmd.substring(iS+1).split(/\s+/);
219 switch(args[0]){
220 case "cache":
221 session.defaultSession.clearCache();
222 return;
223 case "cookie":
224 if(args.length==1){
225 session.defaultSession.clearStorageData({ storages: ['cookies'] });
226 return;
229 let url = args[1];
230 if(url.charCodeAt(0)!==104) url = "https://"+url;
231 session.defaultSession.cookies.get({ url: url }).then((cookies) => {
232 cookies.forEach((cookie) => {
233 session.defaultSession.cookies.remove(targetUrl, cookie.name)})});
235 return;
236 case "dns":
237 session.defaultSession.clearHostResolverCache();
238 return;
239 case "storage":
240 session.defaultSession.clearStorageData();
241 return;
242 default:
244 return;
245 case "exit":
246 win.close();
247 return;
248 case "ext":
249 session.defaultSession.loadExtension(cmd.substring(iS+1));
250 return;
251 case "gr":
252 if(cmd.length==iS) {
253 gredirect_enable(0);
254 return;
256 let i = parseInt(cmd.substring(iS+1));
257 if(i>=0 && i<gredirects.length)
258 gredirect_enable(i);
259 else
260 gredirect_disable();
261 return;
262 case "js"://execute js
263 eval(cmd.slice(4));
264 return;
265 case "nc":
266 bForwardCookie = false;
267 msgbox_info("Cookie forwarding disabled");
268 return;
269 case "uc":
270 if(bForwardCookie) {
271 msgbox_info("Cookie forwarding enabled for global redirection");
272 return;
274 forwardCookie();
275 return;
276 case "np":
277 session.defaultSession.setProxy ({mode:"direct"});
278 bRedirect = true;
279 return;
280 case "up":
281 if(cmd.length>iS)
282 proxy = proxies[cmd.substring(iS+1)]; //retrieve proxy
283 if(proxy){
284 session.defaultSession.setProxy(proxy)
285 .then(() => {gredirect_disable()})
286 .catch((error) => {
287 console.error('Failed to set proxy:', error);
290 return;
291 case "nr":
292 bRedirect = false; return;
293 case "ur":
294 bRedirect = true; return;
295 case "sys":
297 let iHTTP = cmd.search(/https?:\/\//);
298 if(iHTTP<0) return;
299 let iEnd = cmd.indexOf(' ',iHTTP+10);
300 if(iEnd<0) iEnd = cmd.length;
301 let url = cmd.substring(iHTTP,iEnd);
302 let cookies = await session.defaultSession.cookies.get({url: url});
303 let cookieS = cookies.map (cookie => cookie.name + '='
304 + cookie.value ).join(';');
305 let args = cmd.substring(5).split(/\s+/);
306 for(let i=1;i<args.length;i++){
307 let iC = args[i].indexOf('%cookie');
308 if(iC<0) continue;
309 args[i] = args[i].substring(0,i)+cookieS+args[i].substring(i+7);
310 break;
312 const { spawn } = require('child_process');
313 const process = spawn(args[0],args.slice(1));
314 process.stdout.on('data', (data) => {
315 let str = data.toString();
316 console.log(str);
317 let js = "showHtml(`"+str+"`)";
318 win.webContents.executeJavaScript(js,false);
321 return;
322 case "ua":
323 if(cmd.length>iS){
324 let ua = useragents[cmd.substring(iS+1)];
325 if(ua)
326 session.defaultSession.setUserAgent(ua);
327 }else
328 session.defaultSession.setUserAgent(defaultUA);
329 return;
330 case "update":
331 let updateurl;
332 if(cmd.length==iS)
333 updateApp(repositoryurl);
334 else {
335 filename = cmd.substring(iS+1);
336 let iSlash = filename.lastIndexOf('/');
337 if(iSlash>0){
338 let folder = path.join(__dirname,filename.substring(0,iSlash));
339 fs.mkdirSync(folder,{ recursive: true });
341 fetch2file(repositoryurl,filename);
343 return;
348 function gredirect_disable(){
349 if(gredirect){
350 gredirect=null;
351 unregisterHandler();
353 bRedirect = true;
355 function gredirect_enable(i){
356 if(i>=gredirects.length) return;
357 if(!gredirect) registerHandler();
358 gredirect=gredirects[i];
361 function cbConsoleMsg(e, level, msg, line, sourceid){
362 console.log(line);
363 console.log(sourceid);
364 console.log(msg);
367 function interceptRequest(details, callback){
368 let url = details.url;
369 if(58===url.charCodeAt(1) || (!bJS && url.endsWith(".js"))){
370 callback({ cancel: true });
371 return;
373 do {
374 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
375 details.resourceType !== 'subFrame')) break;
376 let oURL = new URL(url);
377 let domain = oURL.hostname;
378 let newUrl;
379 try{
380 let newDomain = redirects[domain];
381 if(!newDomain) break;
382 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
383 }catch(e){break;}
384 callback({ cancel: false, redirectURL: newUrl });
385 return;
386 }while(false);
387 callback({ cancel: false });
390 function cbWindowOpenHandler(details){
391 let url = details.url;
392 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
393 url+"';";
394 switch(details.disposition){
395 case "foreground-tab":
396 case "new-window":
397 js = js + "switchTab(tabs.children.length-1)";
399 win.webContents.executeJavaScript(js,false);
400 return { action: "deny" };
402 function cbTitleUpdate(event,title){
403 win.setTitle(title);
405 function menuSelection(menuTemplate, text){
406 for(let i=0; i<selectMenus.length-1;i++){
407 menuTemplate.push({
408 label: selectMenus[i],
409 click: () => {
410 let cmd = selectMenus[i+1].replace('%s',text);
411 let js = `handleQueries(\`${cmd}\`)`;
412 win.webContents.executeJavaScript(js,false);
417 function menuDownload(menuTemplate, labelprefix, linkUrl){
418 for(let i=0; i<downloadMenus.length-1;i++){
419 menuTemplate.push({
420 label: labelprefix+downloadMenus[i],
421 click: () => {
422 let cmd = downloadMenus[i+1].replace('%u',linkUrl);
423 let js = `handleQueries(\`${cmd}\`)`;
424 win.webContents.executeJavaScript(js,false);
429 function menuArray(labelprefix, linkUrl){
430 let menuTemplate = [
432 label: labelprefix+translate('Open'),
433 click: () => {
434 shell.openExternal(linkUrl);
438 label: labelprefix+translate('Copy'),
439 click: () => {
440 clipboard.writeText(linkUrl);
444 label: labelprefix+translate('Download'),
445 click: () => {
446 win.webContents.downloadURL(linkUrl);
450 if(downloadMenus)
451 menuDownload(menuTemplate, labelprefix, linkUrl);
452 return menuTemplate;
455 function onContextMenu(event, params){
456 let url = params.linkURL;
457 let mTemplate = [];
458 if (url) {
459 mTemplate.push({label:url,enabled:false});
460 mTemplate.push.apply(mTemplate,menuArray("",url));
461 if((url=params.srcURL))
462 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
463 }else if((url=params.srcURL)){
464 mTemplate.push({label:url,enabled:false});
465 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
466 }else if((url=params.selectionText)){
467 menuSelection(mTemplate,url);
468 }else
469 return;
471 const contextMenu = Menu.buildFromTemplate(mTemplate);
472 contextMenu.popup();
475 async function topMenu(){
476 const menuTemplate = [];
477 try {
478 let json = await fs.promises.readFile(path.join(__dirname,'menu.json'), 'utf8');
479 let menus = JSON.parse(json);
480 if(menus.length>1){
481 let submenu = [];
482 for(let i=0;i<menus.length-1; i=i+2){
483 let cmd = menus[i+1];
484 let js = `handleQueries("${cmd}")`;
485 submenu.push({
486 label: menus[i], click: ()=>{
487 win.webContents.executeJavaScript(js,false);
488 }});
490 menuTemplate.push({
491 label: translate('Tools'),
492 submenu: submenu,
495 }catch(e){console.log(e)}
496 menuTemplate.push(
498 label: translate('Edit'),
499 submenu: [
500 { label: translate('Config folder'), click: ()=>{
501 shell.openPath(__dirname);
506 label: translate('Help'),
507 submenu: [
508 { label: translate('Check for updates'), click: ()=>{
509 addrCommand(":update");
511 { label: translate('Help'), accelerator: 'F1', click: ()=>{
512 help();
514 { label: translate('Stop'), accelerator: 'Ctrl+C', click: ()=>{
515 let js="tabs.children[iTab].stop()"
516 win.webContents.executeJavaScript(js,false)
518 { label: translate('getURL'), accelerator: 'Ctrl+G', click: ()=>{
519 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL();getWinTitle()}"
520 win.webContents.executeJavaScript(js,false).then((r)=>{
521 win.setTitle(r);
524 { label: translate('Select'), accelerator: 'Ctrl+L', click:()=>{
525 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
527 { label: translate('New Tab'), accelerator: 'Ctrl+T', click:()=>{
528 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
529 win.webContents.executeJavaScript(js,false);
531 { label: translate('Restore Tab'), accelerator: 'Ctrl+Shift+T', click:()=>{
532 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
533 win.webContents.executeJavaScript(js,false);
535 { label: translate('No redirect'), accelerator: 'Ctrl+R', click: ()=>{
536 gredirect_disable();
538 { label: translate('Redirect'), accelerator: 'Ctrl+Shift+R', click: ()=>{
539 gredirect_enable(0);
541 { label: translate('Close tab'), accelerator: 'Ctrl+W', click: ()=>{
542 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
543 if(""===r) win.close();
544 else win.setTitle(r);
547 { label: translate('Next Tab'), accelerator: 'Ctrl+Tab', click: ()=>{
548 let js="tabInc(1);getWinTitle()";
549 win.webContents.executeJavaScript(js,false).then((r)=>{
550 win.setTitle(r);
553 { label: translate('Previous Tab'), accelerator: 'Ctrl+Shift+Tab', click: ()=>{
554 let js="tabDec(-1);getWinTitle()";
555 win.webContents.executeJavaScript(js,false).then((r)=>{
556 win.setTitle(r);
559 { label: translate('Go backward'), accelerator: 'Alt+Left', click: ()=>{
560 let js="tabs.children[iTab].goBack()";
561 win.webContents.executeJavaScript(js,false);
563 { label: translate('Go forward'), accelerator: 'Alt+Right', click: ()=>{
564 let js="tabs.children[iTab].goForward()";
565 win.webContents.executeJavaScript(js,false);
567 { label: translate('Zoom in'), accelerator: 'Ctrl+Shift+=', click: ()=>{
568 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
569 win.webContents.executeJavaScript(js,false);
571 { label: translate('Zoom out'), accelerator: 'Ctrl+-', click: ()=>{
572 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
573 win.webContents.executeJavaScript(js,false);
575 { label: translate('Default zoom'), accelerator: 'Ctrl+0', click: ()=>{
576 let js="tabs.children[iTab].setZoomFactor(1)";
577 win.webContents.executeJavaScript(js,false);
579 { label: translate('No focus'), accelerator: 'Esc', click: ()=>{
580 let js = `{let e=document.activeElement;
581 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
582 win.webContents.executeJavaScript(js,false);
584 { label: translate('Reload'), accelerator: 'F5', click: ()=>{
585 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
587 { label: translate('Devtools'), accelerator: 'F12', click: ()=>{
588 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
589 win.webContents.executeJavaScript(js,false);
595 const menu = Menu.buildFromTemplate(menuTemplate);
596 Menu.setApplicationMenu(menu);
599 function cmdlineProcess(argv,cwd,extra){
600 let i1st = 2+extra; //index for the first query item
601 if(argv.length>i1st){
602 if(i1st+1==argv.length){//local file
603 let fname = path.join(cwd, argv[i1st]);
604 if(fs.existsSync(fname)){
605 let js = "tabs.children[iTab].src='file://"+fname+"'";
606 win.webContents.executeJavaScript(js,false);
607 win.setTitle(argv[i1st]);
608 return;
611 let url=argv.slice(i1st).join(" ");
612 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
613 win.setTitle(url);
617 async function cbScheme_redir(req){
618 if(!gredirect) return null;
619 let oUrl = req.url;
620 let newurl = gredirect+oUrl;
621 const parsedUrl = url.parse(newurl);
622 let headers = new Headers();
623 for (var pair of req.headers.entries())
624 headers.set(pair[0],pair[1]);
625 if(bForwardCookie){
626 let cookies = await session.defaultSession.cookies.get({url: oUrl});
627 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
628 headers.set('cookie',cookieS);
630 //missing referer header
631 //headers.set('referer',);
632 const options = {
633 hostname: parsedUrl.hostname,
634 port: parsedUrl.port,
635 path: parsedUrl.path,
636 method: req.method,
637 headers: headers
639 return new Promise(async (resolve, reject) => {
640 const nreq = https.request(options, (res) => {
641 let body = [];
642 res.on('data', (chunk) => {
643 body.push(chunk);
646 res.on('end', () => {
647 try {
648 body = Buffer.concat(body);
649 const response = new Response(body, res);
650 resolve(response);
651 } catch (e) {
652 reject(e);
656 nreq.on('error', (err) => {
657 reject(err);
659 if (req.body){
660 try {
661 const reader = req.body.getReader();
662 do {
663 const { done, value } = await reader.read();
664 if (done) {
665 nreq.end();
666 break;
668 nreq.write(value);
669 console.log(headers);
670 console.log(new TextDecoder("iso-8859-1").decode(value));
671 }while (true);
672 }catch(e){reject(e)}
673 }else
674 nreq.end();
678 function registerHandler(){
679 protocol.handle("http",cbScheme_redir);
680 protocol.handle("https",cbScheme_redir);
681 protocol.handle("ws",cbScheme_redir);
682 protocol.handle("wss",cbScheme_redir);
684 function unregisterHandler(){
685 protocol.unhandle("http",cbScheme_redir);
686 protocol.unhandle("https",cbScheme_redir);
687 protocol.unhandle("ws",cbScheme_redir);
688 protocol.unhandle("wss",cbScheme_redir);
691 function forwardCookie(){
692 const choice = dialog.showMessageBoxSync(null, {
693 type: 'warning',
694 title: 'Confirm cookie forwarding with global redirection',
695 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
696 buttons: ['No','Yes']
698 if(1===choice) bForwardCookie=true;
700 function msgbox_info(msg){
701 dialog.showMessageBoxSync(null, {
702 type: 'info',
703 title: msg,
704 message: msg,
705 buttons: ['OK']
709 async function updateApp(url){//url must ending with "/"
710 let msg;
711 do {
712 try {
713 let res = await fetch(url+"package.json");
714 let packageS = await res.text();
715 {//the last part of version string is the version number, must keep increasing
716 let head = packageS.slice(2,40);
717 let iV = head.indexOf("version");
718 if(iV<0) {
719 msg = "remote package.json corrupted"
720 break;
722 iV = iV + 10;
723 let iE = head.indexOf('"',iV+4);
724 let iS = head.lastIndexOf('.',iE-1);
725 let nLatestVer = parseInt(head.substring(iS+1,iE));
727 let ver = app.getVersion();
728 iS = ver.lastIndexOf('.');
729 let nVer = parseInt(ver.substring(iS+1));
730 if(nVer>=nLatestVer){
731 msg = `Current version ${ver} is already up to date`;
732 break;
734 const choice = dialog.showMessageBoxSync(null, {
735 type: 'warning',
736 title: `Update from ${url}`,
737 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
738 buttons: ['YES','NO']
740 if(1===choice) return;
743 writeFile("package.json", packageS);
745 fetch2file(url,"webview.js");
746 fetch2file(url,"index.html");
747 msg = "Update completed";
748 }catch(e){
749 msg = "Fail to update"
751 }while(false);
752 dialog.showMessageBoxSync(null, {
753 type: 'info',
754 title: `Update from ${url}`,
755 message: msg,
756 buttons: ['OK']
760 async function fetch2file(urlFolder, filename, bOverwritten=true){
761 let pathname=path.join(__dirname,filename);
762 if(!bOverwritten && fs.existsSync(pathname)) return;
763 let res = await fetch(urlFolder+filename);
764 let str = await res.text();
765 writeFile(pathname, str);
768 async function writeFile(filename, str){
769 let pathname=filename+".new";
770 fs.writeFile(pathname, str, (err) => {
771 if(err) throw "Fail to write";
772 fs.rename(pathname,filename,(e1)=>{
773 if(e1) throw "Fail to rename";
778 function help(){
779 const readme = "README.md";
780 const htmlFN = path.join(__dirname,readme);
781 let js=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
782 win.webContents.executeJavaScript(js,false)
785 function downloadContextMenuTemp(url){
786 let mTemplate =
787 [{label:url,enabled:false},
788 {label: translate('Download')},
790 label: translate('Copy'),
791 click: () => {
792 clipboard.writeText(url);
796 menuDownload(mTemplate, "", url);
797 return mTemplate;
799 async function initTranslateRes(lang){
800 let basename=path.join(__dirname,"translate.");
801 let fname = basename+lang;
802 if(!fs.existsSync(fname))
803 fname = basename+lang.slice(0,2);
804 try {
805 let json = await fs.promises.readFile(fname,'utf8');
806 translateRes = JSON.parse(json);
807 } catch (e){}
808 topMenu();
811 function translate(str){
812 let result;
813 if(translateRes && (result=translateRes[str])) return result;
814 return str;
817 function promiseContextMenu(menuTemplate) {
818 return new Promise((resolve, reject) => {
819 menuTemplate[1].click = () => resolve(-1);
820 const menu = Menu.buildFromTemplate(menuTemplate);
821 menu.on('menu-will-close', () => resolve(-2));
822 menu.popup();
826 function httpReq(url, method, filePath){
827 fs.readFile(filePath, (err, fileData) => {
828 if (err) {
829 console.error(`Error reading file: ${err.message}`);
830 return;
833 let opts = {
834 method: method,
835 headers: {
836 "Content-Type":'application/octet-stream',
838 body: fileData,
840 fetch(url,opts);
844 function bangcommand(q,offset){
845 let iS = q.indexOf(' ',offset);
846 if(iS<0) iS=q.length;
847 let fname = q.substring(offset,iS);
848 let fpath = path.join(__dirname,fname+'.js');
849 if (fs.existsSync(fpath)) {
850 fs.readFile(fpath, 'utf8',(err, js)=>{
851 if (err) {
852 console.log(err);
853 return;
855 const prefix = "(function(){";
856 const postfix = "})(`";
857 const end ="`)";
858 const fjs = `${prefix}${js}${postfix}${q}${end}`;
859 eval(fjs);