uweb/misc/ebrowser/index.html
2024-07-12 11:06:22 +08:00

767 lines
21 KiB
HTML

<!--
Copyright (C) 2024 Richard Hao Cao
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.
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.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html><html><head><meta charset="UTF-8">
<style>
html{
height: 100%;
overflow: hidden;
}
body{
display: flex;
flex-direction: column;
height: 100%;
margin-top: 1px;
}
div.webviews{
display: flex;
flex-direction: column;
flex-grow:1;
}
webview{display: none;width:100%;height:100%}
.curWV{display: inherit !important;}
.autocomplete-active {
background-color: DodgerBlue !important;
color: #ffffff;
}
.invis{display: none}
/*the container must be positioned relative:*/
.autocomplete {
position: relative;
display: inline-block;
width:100%;
}
.autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.autocomplete-items div {
cursor: pointer;
background-color: #fff;
}
.autocomplete-items div:hover {
background-color: #e9e9e9;
}
</style>
<script>
const { ipcRenderer } = require('electron');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
var iTab = 0;
var tabs;
var engines = {};
var mapKeys = {};
var closedUrls = [];
var autocStrArray = [];
var defaultSE = "https://www.bing.com/search?q=%s";
var historyFile = path.join(__dirname,'history.rec');
var bHistory = false;
var bQueryHistory = false;
let sitecssP = path.join(__dirname,"sitecss");
let sitejsP = path.join(__dirname,"sitejs");
let gjsA = [];
let gcssA = [];
let gcssJSA = [];
var bDomainJS = fs.existsSync(sitejsP);
var bDomainCSS = fs.existsSync(sitecssP);
var autocMode = 0; //0 for substring, 1 for startsWith
const BML_head = "(async ()=>{let d=document;async function _loadJs(u){var a=d.createElement('script');a.type='text/javascript';a.async=false;a.src=u;d.body.appendChild(a);await new Promise(resolve=>a.onload=resolve)}";
const BML_tail = "})()";
const BML_md = BML_head + "await _loadJs('https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js');let b=d.body;b.innerHTML=marked.parse(b.textContent)})()";
let lastKeys;
let lastKeys_millis = 0;
var lastVal;
fs.readFile(path.join(__dirname,'search.json'), 'utf8', (err, jsonString) => {
if (err) {
coloncommand(":js fetch2file(repositoryurl,'search.json')");
return;
}
initSearchEngines(jsonString,false);
});
fs.readFile(path.join(__dirname,'mapkeys.json'), 'utf8', (err, jsonStr) => {
if (err) {
coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
return;
}
try {
mapKeys = JSON.parse(jsonStr);
}catch(e){}
});
appendAutoc_rec(path.join(__dirname,'default.autoc'),null);
appendAutoc_rec(path.join(__dirname,'bookmark.rec'),' ');
function initSearchEngines(jsonStr){
try{
let val1st;
engines=JSON.parse(jsonStr, (key, value)=>{
if(!val1st && !(/^\d+$/u.test(key))) val1st=value;
return value;
});
if(val1st) defaultSE=val1st;
}catch(e){}
}
function save(filePath, u8array){
//alert(Object.prototype.toString.call(u8array))
fs.writeFile (filePath, u8array, (err) => {
if (err) {
console.error (err);
return;
}
});
}
function print2PDF(filePath, options){
tabs.children[iTab].printToPDF(options)
.then(u8array=>save(filePath,u8array));
}
function bookmark(args){//b [filenamestem] url title :bookmark
let bmFileName = "bookmark.rec";
let tab = tabs.children[iTab];
let url = tab.getURL();
if(args.length>1)
bmFileName = args[1]+".rec";
let title = tab.getTitle();
let line = title + " " + url + "\n";
fs.appendFile(path.join(__dirname,bmFileName), line, (err)=>{});
}
function switchTab(i){
let tab = tabs.children[iTab];
if(document.activeElement == tab) tab.blur();
tab.classList.remove('curWV');
iTab = i;
tabs.children[iTab].classList.add('curWV');
}
async function loadJSFile(tab,jsF){
try {
let js = await fs.promises.readFile(jsF,'utf8');
tab.executeJavaScript(js,false);
}catch(e){}
}
async function loadCSSFile(tab,jsF){
try {
let js = await fs.promises.readFile(jsF,'utf8');
tab.insertCSS(css);
}catch(e){}
}
function cbStartLoading(e){
if(!bDomainCSS) return;
let tab = e.target;
let domain;
try{
domain = new URL(tab.getURL()).hostname;
}catch(e){return;}
let jsF = path.join(sitecssP, domain+".js");
loadJSFile(tab,jsF);
jsF = path.join(sitecssP, domain+".css");
loadCSSFile(tab,jsF);
gcssA.forEach((fname)=>{
jsF = path.join(__dirname, fname);
loadCSSFile(tab,jsF);
});
gcssJSA.forEach((fname)=>{
jsF = path.join(__dirname, fname);
loadJSFile(tab,jsF);
});
}
function cbNavigate(e){
let url = e.url;
if(58===url.charCodeAt(1)){
e.preventDefault();
if(confirm("Proceed to execute risky operations: "+url))
internalLink(url);
else
document.forms[0].q.value = url;
}
}
function cbFinishLoad(e){
let tab = e.target;
let url = tab.getURL();
if(bHistory){
let histItem = tab.getTitle()+" "+url+"\n";
fs.appendFile(historyFile, histItem, (err) => {});
}
let js = tab.dataset.jsonce;
if(js){
tab.dataset.jsonce = null;
tab.executeJavaScript(js,false);
}
if(bDomainJS){
let domain = new URL(url).hostname;
let jsF = path.join(sitejsP, domain+".js");
loadJSFile(tab,jsF);
}
gjsA.forEach((fname)=>{
let jsF = path.join(__dirname, fname);
loadJSFile(tab,jsF);
});
}
function initTab(tab){
tab.allowpopups = true;
tab.addEventListener('did-finish-load',cbFinishLoad);
tab.addEventListener('did-start-loading',cbStartLoading);
tab.addEventListener('will-navigate',cbNavigate);
}
function newTab(){
var tab = document.createElement('webview');
initTab(tab);
tabs.appendChild(tab);
}
function tabInc(num){
let nTabs = tabs.children.length;
if(nTabs<2) return;
let i = iTab +num;
if(i>=nTabs) i=0;
switchTab(i);
}
function tabDec(num){
let nTabs = tabs.children.length;
if(nTabs<2) return;
let i = iTab +num;
if(i<0) i=nTabs-1;
switchTab(i);
}
function tabClose(){
let nTabs = tabs.children.length;
if(nTabs<2) return "";//no remain tab
let tab = tabs.children[iTab];
closedUrls.push(tab.getURL());
if(document.activeElement == tab) tab.blur();
tabs.removeChild(tab);
nTabs--;
if(iTab>=nTabs) iTab=iTab-1;
tabs.children[iTab].classList.add('curWV');
return getWinTitle();
}
function tabJS(js){
tabs.children[iTab].executeJavaScript(js,false);
}
function getWinTitle(){
let t=tabs.children[iTab];
let title = (iTab+1) + '/' + tabs.children.length;
try{title=title+' '+t.getTitle()+' '+t.getURL()}catch(e){}
return title
}
async function appendAutoc_rec(filename, delimit){
try{
const readInterface = readline.createInterface ({
input: fs.createReadStream (filename, 'utf8'),
});
for await (const line of readInterface) {
let iS;
if(delimit && (iS=line.lastIndexOf(delimit))>0){
autocStrArray.push(line.substring(iS+1));
}else
autocStrArray.push(line);
}
lastVal = null; //trigger full search
}catch(e){return;}
}
function keyPress(e){
var inputE = document.forms[0].q;
if (e.altKey||e.metaKey)
return;
var key = e.key;
if(e.ctrlKey){
switch(key){
case "Home":
tabJS("window.scrollTo(0,0)");
return;
case "End":
tabJS("window.scrollTo(0,document.body.scrollHeight)");
return;
}
return;
}
SCROLL: do {
let h = -32;
switch(key){
case " ":
if(inputE === document.activeElement) return;
if(e.shiftKey){
h = -3*document.documentElement.clientHeight/4;
break;
}
case "PageDown":
h = 3*document.documentElement.clientHeight/4;
break;
case "PageUp":
h = -3*document.documentElement.clientHeight/4;
break;
case "ArrowDown":
h = 32;
case "ArrowUp":
if(inputE === document.activeElement &&
0!==inputE.nextElementSibling.children.length)
return;
break;
default:
break SCROLL;
}
let js = `javascript:window.scrollBy(0,${h})`;
tabJS(js);
return;
}while(false);
if(inputE === document.activeElement){
if (9===e.keyCode){
e.preventDefault();
tabs.children[iTab].focus();
}
return;
}
var curMillis = Date.now();
if(curMillis-lastKeys_millis>1000)
lastKeys = null;
lastKeys_millis = curMillis;
switch (key) {
case "!":
case "/":
case ":":
inputE.value = "";
inputE.focus();
lastKeys = null;
return;
}
lastKeys = !lastKeys ? key : lastKeys + key;
let cmds = mapKeys[lastKeys];
if(cmds){//try to run cmds
let keyLen = lastKeys.length;
setTimeout(()=>{
if(lastKeys.length != keyLen) return;
lastKeys = null;
handleQueries(cmds);
}, 500);
}
}
function getQ(){return document.forms[0].q.value;}
function bang(query, iSpace){
let se=defaultSE;
{
let name = query.slice(0,iSpace);
let engine = engines[name];
if(engine){
se = engine;
query = query.substring(iSpace+1);
}
}
return se.replace('%s',query);
}
function coloncommand(q){
ipcRenderer.send("command",q);
}
function coloncommand_render(cmd){
args = cmd.substring(1).split(/\s+/);
switch(args[0]){
case "ac":
autoc(args);
return;
case "b":
bookmark(args);
return;
case "bjs":
eval(cmd.slice(5));
return;
case "bml":
bml(args);
return;
case "pdf":
savePdf(args);
return;
}
}
function autoc(args){
if(2!=args.length) return;
let fpath = path.join(__dirname,args[1]);
let fname = fpath;
let delimit = ' ';
if (!fs.existsSync(fname)){
fname = fpath+".autoc";
if (!fs.existsSync(fname))
fname = fpath+".rec";
else
delimit = null;
}
appendAutoc_rec(fname,delimit);
}
function bml(args){
if(2!=args.length) return;
let filename = args[1]+".js";
fs.readFile(path.join(__dirname,filename), 'utf8', (err,str) => {
if (err) return;
tabs.children[iTab].executeJavaScript(str,false);
});
}
function savePdf(args){
let filename = "ebrowser.pdf";
let options = {};
if(1<args.length){
let c0 = args[1].charCodeAt(0);
let i = 1;
if(123!=c0){//not '{' options then it is filename
filename = args[1] + ".pdf";
i = 2;
}
if(i==args.length-1){//:Pdf [filename] {...}
if(2==args[i].length){// '{}'
let width = document.body.clientWidth/96;
tabs.children[iTab].executeJavaScript("document.documentElement.scrollHeight",
false).then((h)=>{
let opts = {
printBackground:true,
pageSize:{width:width,height:h/96}};
print2PDF(filename,opts);
});
return;
}else{
try {
options = JSON.parse(args[i]);
}catch(e){};
}
}
}
print2PDF(filename,options);
}
function bangcommand(q){
let iS = q.indexOf(' ',1);
if(iS<0) iS=q.length;
let fname = q.substring(1,iS);
let fpath = path.join(__dirname,fname+'.js');
if (fs.existsSync(fpath)) {
fs.readFile(fpath, 'utf8',(err, js)=>{
if (err) {
console.log(err);
return;
}
const prefix = "(function(){";
const postfix = "})(`";
const end ="`)";
const fjs = `${prefix}${js}${postfix}${q}${end}`;
eval(fjs);
});
}
}
function recQueryHistory(q){
if(bQueryHistory)
fs.appendFile(path.join(__dirname,"history.autoc"), q, (err)=>{});
}
function handleQuery(q){
if(q.length>1){
let c0=q.charCodeAt(0);
let c1=q.charCodeAt(1);
switch(c0){
case 33://"!"
bangcommand(q);
recQueryHistory(q);
return;
case 47://"/"
tabs.children[iTab].findInPage(q.substring(1));
return;
case 58://':'
if(c1>98 && 112!=c1)
coloncommand(q);
else
coloncommand_render(q);
recQueryHistory(q);
return;
}
if(58===c1){
internalLink(q);
return;
}
}
var url=q;
NOREC: do{
do {
if(q.length>12){
let c6 = q.charCodeAt(6);
if(47===c6){// '/'
let c5 = q.charCodeAt(5);
if(47===c5 && 58===q.charCodeAt(4))//http/file urls
break NOREC;
if(58===c5 && 47===q.charCodeAt(7))//https://
break NOREC;
}else if(q.startsWith("javascript:")){
tabs.children[iTab].executeJavaScript(q.substring(11),false);
recQueryHistory(q);
return;
}else if(q.startsWith("view-source:")) break;
else if(q.startsWith("data:")) break;
}
let iS = q.indexOf(' ');
if(iS<0){
if(q.length>5 && 58===q.charCodeAt(5)){// about:
break;
}
if(q.indexOf('.')>0){
url = 'https://'+q;
break NOREC;
}
url = defaultSE.replace('%s',q);
break;
}
url = bang(q, iS);
if(58===url.charCodeAt(1)){
internalLink(url);
recQueryHistory(q);
return;
}
}while(false);
recQueryHistory(q);
}while(false);
tabs.children[iTab].src=url;
}
function internalLink(url){
let cmd = url.charCodeAt(2);
let subcmd = url.charCodeAt(3);
let iColon = url.indexOf(':',5);
let name = decodeURIComponent(url.slice(4,iColon));
let rurl = url.substring(iColon+1);
switch(cmd){
case 48://'0' i:0
switch(subcmd){
case 47://'/' i:0/
if(106===url.charCodeAt(4) && 115===url.charCodeAt(5) && 47===url.charCodeAt(6)){
//i:0/js/xx:[url]
let fname = name;
let pname = path.join(__dirname,fname);
if(fs.existsSync(pname)){
(async ()=>{
try {
let js = await fs.promises.readFile(pname,'utf8');
let t=tabs.children[iTab];
t.dataset.jsonce=js;
t.src = rurl;
}catch(e){}
})();
}
}
return;
case 48: //i:00
{
let endS;
let is = url.indexOf('%s',iColon+1);
if(is<0)
endS = '%s"\n';
else
endS = '"\n';
let pname = path.join(__dirname,"search.json");
let str = '"'+name+'":"'+rurl+
endS;
jsonAppend(pname,125,str);
}
return;
case 49: //i:01
{
let pname = path.join(__dirname,"menu.json");
let str = '"'+name+'",":bjs handleQuery(`'+
rurl+'${tabs.children[iTab].getURL()}`)"\n';
jsonAppend(pname,93,str);
}
return;
case 50: //i:02
{
let pname = path.join(__dirname,"uas.json");
let str = '"'+name+'":"'+rurl+'"\n';
jsonAppend(pname,125,str);
}
return;
default:
}
}
}
function handleQueries(cmds){
for(var cmd of cmds.split("\n"))
handleQuery(cmd);
}
async function jsonAppend(filePath, charcode, str){
let fd;
try{
fd = await fs.promises.open(filePath, 'r+');
}catch(e){
try {
fd = await fs.promises.open(filePath, 'w+');
}catch(e1){return}
}
try{
const stats = await fd.stat();
const fileSize = stats.size;
const buffer = Buffer.alloc(1);
let position = fileSize-1;
while (position >= 0) {
await fd.read(buffer, 0, 1, position);
if (buffer[0] === charcode) break;
position--;
}
let endS = String.fromCharCode(charcode);
if(position<0){//re-write whole file
str = String.fromCharCode(charcode-2)+str+endS;
position = 0;
}else
str = ","+str+endS;
await fd.truncate(position);
const buf = Buffer.from(str);
await fd.write(buf, 0, buf.length, position);
await fd.close();
}catch(e){console.log(e)}
}
function autocomplete(inp,container,arr) {
var currentFocus;
function clickItem(e){inp.value = arr[e.target.dataset.index];}
function appendElement(el,dataindex){
el.dataset.index = dataindex;
el.addEventListener("click", clickItem);
container.appendChild(el);
}
function itemInnerHTML(b,str,val,iStr){
b.innerHTML = str.substr(0,iStr);
b.innerHTML += "<strong>" + str.substr(iStr, val.length) + "</strong>";
b.innerHTML += str.substr(iStr+val.length);
}
inp.addEventListener("input", function(e) {
const MAXITEMS = 30;
var b, i, val = this.value;
if (!val) { return false;}
currentFocus = -1;
let items = container.children;
let iBase = 0;
let lastV = lastVal;
lastVal = val;
if(val.startsWith(lastV)){
let itemsLen = items.length;
if(itemsLen<=0) return;
i = itemsLen -1;
iBase = items[i].dataset.index +1;
switch(autocMode){
case 0:
for (; i>=0; i--) {
b = items[i];
let str = arr[b.dataset.index];
let iStr = str.indexOf(val);
if(iStr<0) {
b.parentNode.removeChild(b);
continue;
}
itemInnerHTML(b,str,val,iStr);
}
break;
case 1:
for (; i>=0; i--) {
b = items[i];
let str = arr[b.dataset.index];
let oLen = lastval.length;
if(!str.startsWith(val.substring(oLen),oLen)) {
b.parentNode.removeChild(b);
continue;
}
itemInnerHTML(b,str,val,0);
}
break;
}
if(itemsLen<MAXITEMS)
return;
else
if(container.children.length>=MAXITEMS) return;
}else
closeAllLists();
i = iBase;
switch(autocMode){
case 0:
for (; i < arr.length; i++) {
let iStr = arr[i].indexOf(val);
if(iStr<0) continue;
{
b = document.createElement("DIV");
itemInnerHTML(b,arr[i],val,iStr);
appendElement(b,i);
if(container.children.length>=MAXITEMS) break;
}
}
return;
case 1://startsWith
for (; i < arr.length; i++) {
if (arr[i].startsWith(val)) {
b = document.createElement("DIV");
itemInnerHTML(b,arr[i],val,0);
appendElement(b,i);
if(container.children.length>=MAXITEMS) break;
}
}
}
});
inp.addEventListener("keydown", function(e) {
var x = container.getElementsByTagName("div");
if (0===x.length) return false;
if (e.keyCode == 40) {//downarrow
currentFocus++;
addActive(x);
} else if (e.keyCode == 38) { //up
currentFocus--;
addActive(x);
} else if (e.keyCode == 13) {
if (currentFocus > -1) {
e.preventDefault();
if (x) x[currentFocus].click();
currentFocus = -1;
}
closeAllLists();
lastVal = null;
}
});
function addActive(x) {
removeActive(x);
if (currentFocus >= x.length) currentFocus = 0;
if (currentFocus < 0) currentFocus = (x.length - 1);
x[currentFocus].classList.add("autocomplete-active");
}
function removeActive(x) {
for (var i = 0; i < x.length; i++) {
x[i].classList.remove("autocomplete-active");
}
}
function closeAllLists() {
container.innerHTML = '';
}
inp.addEventListener("blur", function () {
setTimeout(()=>container.classList.add("invis"),200);
});
inp.addEventListener("focus", function () {
container.classList.remove("invis");
});
}
</script>
</head>
<body>
<form class="autocomplete" autocomplete="off" action="javascript:handleQuery(getQ())">
<input type="text" name=q style="width:100%" autofocus>
<div class="autocomplete-items"></div>
</form>
<div class="webviews">
<webview class="curWV" allowpopups></webview>
</div>
<script>
tabs = document.body.children[1];
initTab(tabs.children[0]);
{
let inp = document.forms[0].q;
autocomplete(inp,inp.nextElementSibling,autocStrArray);
}
document.addEventListener('keydown', keyPress);
</script>
</body></html>