769 lines
39 KiB
JavaScript
769 lines
39 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Original nightTab source (input) lives next to this script.
|
||
const root = path.join(__dirname, 'quelle-nightTab');
|
||
// Output HTML: explicit arg, else default to the WebView2 project.
|
||
const out = process.argv[2] || path.join(__dirname, 'webview2', 'app-index.html');
|
||
const APPNAME = 'MyStart';
|
||
|
||
let css = fs.readFileSync(path.join(root, 'index.41b2d2a8cee846b032b5.css'), 'utf8');
|
||
|
||
const mime = { woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf' };
|
||
css = css.replace(/url\((font\/[^)]+)\)/g, (m, rel) => {
|
||
const p = path.join(root, rel);
|
||
const ext = rel.split('.').pop();
|
||
const b64 = fs.readFileSync(p).toString('base64');
|
||
return `url(data:${mime[ext] || 'application/octet-stream'};base64,${b64})`;
|
||
});
|
||
|
||
let initJs = fs.readFileSync(path.join(root, 'initialBackground.js'), 'utf8');
|
||
let mainJs = fs.readFileSync(path.join(root, 'index.09153abdbaa791fa4f0e.js'), 'utf8');
|
||
|
||
// --- 1) Remove the "Support" and "Coffee" entries from the settings menu ---
|
||
const navSnippet = '{name:"Support",active:!1,overscroll:!1},{name:"Coffee",active:!1,overscroll:!1},';
|
||
if (!mainJs.includes(navSnippet)) throw new Error('navData snippet not found — aborting');
|
||
mainJs = mainJs.replace(navSnippet, '');
|
||
|
||
// --- 1b) Rebuild the About page: logo + name + version + own website & git links ---
|
||
const aStart = 'const t=new Pa({text:"GitHub.';
|
||
const aEndMarker = ',s,o,n]))';
|
||
const i = mainJs.indexOf(aStart);
|
||
if (i < 0) throw new Error('About block start not found — aborting');
|
||
const j = mainJs.indexOf(aEndMarker, i);
|
||
if (j < 0) throw new Error('About block end not found — aborting');
|
||
const aboutBody =
|
||
'const t=new Pa({text:"m-viper.de",href:"https://m-viper.de",openNew:!0}),' +
|
||
'a=new Pa({text:"git.viper.ipv64.net",href:"https://git.viper.ipv64.net",openNew:!0}),' +
|
||
's=y("p");s.innerHTML=`Website: ${t.link().outerHTML}`;' +
|
||
'const o=y("p");o.innerHTML=`Git: ${a.link().outerHTML}`,' +
|
||
'e.appendChild(y("div",[y("div|class:version",[_t.render(),' +
|
||
'y("div|class:version-details",[y("h1:MyStart|class:version-app-name"),' +
|
||
'y("p:Version 1.0.4|class:version-number")])]),y("hr"),s,o,y("div|id:mystart-autostart-row,class:mystart-autostart-row")]))';
|
||
mainJs = mainJs.slice(0, i) + aboutBody + mainJs.slice(j + aEndMarker.length);
|
||
|
||
// --- 2) Rename branding (visible text + storage keys) consistently ---
|
||
const rename = (s) => s.split('nightTab').join(APPNAME);
|
||
mainJs = rename(mainJs);
|
||
initJs = rename(initJs);
|
||
|
||
// --- 3) Import: accept BOTH original nightTab backups and MyStart backups ---
|
||
// After rename the two validation gates only accept the "MyStart" marker key;
|
||
// re-add acceptance of the original "nightTab" marker so old backups import too.
|
||
const importGates = [
|
||
['JSON.parse(t).MyStart||JSON.parse(t)[nt.toLowerCase()]',
|
||
'JSON.parse(t).MyStart||JSON.parse(t).nightTab||JSON.parse(t)[nt.toLowerCase()]'],
|
||
['JSON.parse(r.target.result).MyStart||JSON.parse(r.target.result)[nt.toLowerCase()]',
|
||
'JSON.parse(r.target.result).MyStart||JSON.parse(r.target.result).nightTab||JSON.parse(r.target.result)[nt.toLowerCase()]'],
|
||
];
|
||
for (const [from, to] of importGates) {
|
||
if (!mainJs.includes(from)) throw new Error('import gate not found: ' + from);
|
||
mainJs = mainJs.replace(from, to);
|
||
}
|
||
|
||
// --- 4) German UI: translate the DISPLAY of menu tabs/sub-tabs/headers only.
|
||
// The underlying name stays English because it is used as a routing key
|
||
// (makeId = first word lowercased). __TR maps display text, keyed by that id.
|
||
const menuMap = {
|
||
theme:'Design', layout:'Layout', header:'Kopfzeile', bookmark:'Lesezeichen',
|
||
group:'Gruppe', toolbar:'Werkzeugleiste', data:'Daten', mystart:'MyStart',
|
||
preset:'Vorlage', saved:'Gespeichert', style:'Stil', colour:'Farbe',
|
||
accent:'Akzent', font:'Schrift', radius:'Radius', shadow:'Schatten',
|
||
shade:'Schattierung', opacity:'Deckkraft', background:'Hintergrund',
|
||
search:'Suche', scaling:'Skalierung', area:'Bereich', padding:'Innenabstand',
|
||
gutter:'Abstand', alignment:'Ausrichtung', page:'Seite', greeting:'Begrüßung',
|
||
transitional:'Übergangswörter', clock:'Uhr', date:'Datum', general:'Allgemein',
|
||
orientation:'Ausrichtung', sort:'Sortierung', name:'Name', size:'Größe',
|
||
location:'Ort', position:'Position', controls:'Steuerung',
|
||
restore:'Wiederherstellen', backup:'Sichern', clear:'Löschen',
|
||
};
|
||
const trInfra =
|
||
'window.__MT=' + JSON.stringify(menuMap) + ';' +
|
||
'window.__TR=function(s){if(s==null)return s;var k=String(s).split(" ")[0].toLowerCase();return window.__MT[k]||s;};';
|
||
|
||
// Autostart toggle: renders a CUSTOM slider switch into the About page's
|
||
// #mystart-autostart-row (native checkboxes are hidden by nightTab's CSS:
|
||
// opacity:0/position:absolute/pointer-events:none). Talks to the native host
|
||
// via postMessage; the host reads/writes the HKCU Run key.
|
||
const autostartJs = `(function(){
|
||
var A={enabled:false,supported:!!(window.chrome&&window.chrome.webview)};
|
||
window.__mystartAutostart=A;
|
||
// Second switch: OneDrive sync, opt-in (off until switched on here).
|
||
var Y={enabled:false,available:true,path:null,custom:false};
|
||
function paintOne(prefix,on){
|
||
var tr=document.getElementById(prefix+'-track');
|
||
var kn=document.getElementById(prefix+'-knob');
|
||
if(!tr||!kn)return;
|
||
tr.style.background=on?'#2ea043':'#565d68';
|
||
kn.style.left=on?'23px':'3px';
|
||
}
|
||
function paint(){
|
||
paintOne('mystart-autostart',A.enabled);
|
||
paintOne('mystart-sync',Y.enabled);
|
||
var w=document.getElementById('mystart-sync-wrap');
|
||
if(w){
|
||
w.style.opacity=Y.available?'1':'0.5';
|
||
w.style.cursor=Y.available?'pointer':'default';
|
||
w.title=Y.available?'':'Kein Sync-Ordner gefunden – bitte unten einen Ordner wählen.';
|
||
}
|
||
var fp=document.getElementById('mystart-sync-folder-path');
|
||
if(fp)fp.textContent=Y.path?('Ordner: '+Y.path+(Y.custom?'':' (automatisch, OneDrive)')):'Kein OneDrive-Ordner gefunden – bitte einen Ordner wählen.';
|
||
var rs=document.getElementById('mystart-sync-folder-reset');
|
||
if(rs)rs.style.display=Y.custom?'':'none';
|
||
}
|
||
if(A.supported){
|
||
window.chrome.webview.addEventListener('message',function(ev){
|
||
var d=ev.data;if(!d)return;
|
||
if(d.type==='autostart'){A.enabled=!!d.enabled;paint();}
|
||
else if(d.type==='sync-state'){Y.enabled=!!d.enabled;Y.available=!!d.available;Y.path=d.path||null;Y.custom=!!d.custom;paint();}
|
||
});
|
||
}
|
||
function mkToggle(row,prefix,label,hint,onClick){
|
||
var wrap=document.createElement('div');
|
||
wrap.id=prefix+'-wrap';
|
||
wrap.style.cssText='display:flex;align-items:center;gap:10px;margin-top:18px;cursor:pointer;user-select:none;';
|
||
var track=document.createElement('div');
|
||
track.id=prefix+'-track';
|
||
track.style.cssText='position:relative;width:46px;height:26px;border-radius:13px;background:#565d68;flex:0 0 auto;transition:background .15s;box-sizing:border-box;';
|
||
var knob=document.createElement('div');
|
||
knob.id=prefix+'-knob';
|
||
knob.style.cssText='position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:left .15s;box-shadow:0 1px 2px rgba(0,0,0,.4);box-sizing:border-box;';
|
||
track.appendChild(knob);
|
||
var txt=document.createElement('span');
|
||
txt.textContent=label;
|
||
if(!A.supported){wrap.style.opacity='0.5';wrap.style.cursor='default';wrap.title='Nur in der MyStart-App verfügbar.';}
|
||
wrap.appendChild(track);wrap.appendChild(txt);row.appendChild(wrap);
|
||
if(hint){
|
||
var h=document.createElement('p');
|
||
h.textContent=hint;
|
||
h.style.cssText='margin:6px 0 0 56px;font-size:12px;opacity:.65;';
|
||
row.appendChild(h);
|
||
}
|
||
wrap.addEventListener('click',function(){if(A.supported)onClick();});
|
||
}
|
||
function render(row){
|
||
mkToggle(row,'mystart-autostart','Mit Windows starten (minimiert)',null,function(){
|
||
A.enabled=!A.enabled;paint();
|
||
try{window.chrome.webview.postMessage({type:'autostart-set',value:A.enabled});}catch(e){}
|
||
});
|
||
mkToggle(row,'mystart-sync','Zwischen PCs abgleichen (OneDrive)','Gleicht Lesezeichen und Reiter über einen synchronisierten Ordner ab, standardmäßig OneDrive. Auf jedem PC einzeln einschalten, und auf allen PCs denselben Ordner nutzen.',function(){
|
||
if(!Y.available)return;
|
||
Y.enabled=!Y.enabled;paint();
|
||
try{window.chrome.webview.postMessage({type:'sync-set',value:Y.enabled});}catch(e){}
|
||
});
|
||
// Which folder the sync files go to: shown, plus pick another / go back to OneDrive.
|
||
var fr=document.createElement('div');
|
||
fr.style.cssText='margin:8px 0 0 56px;font-size:12px;';
|
||
var fpath=document.createElement('div');
|
||
fpath.id='mystart-sync-folder-path';
|
||
fpath.style.cssText='opacity:.75;word-break:break-all;';
|
||
var fbtns=document.createElement('div');
|
||
fbtns.style.cssText='display:flex;gap:8px;margin-top:6px;flex-wrap:wrap;';
|
||
function mkBtn(text,id,type){
|
||
var b=document.createElement('div');
|
||
if(id)b.id=id;
|
||
b.textContent=text;
|
||
b.style.cssText='padding:5px 10px;border-radius:6px;background:rgba(128,128,128,.25);cursor:pointer;user-select:none;';
|
||
b.onmouseenter=function(){b.style.background='rgba(128,128,128,.4)';};
|
||
b.onmouseleave=function(){b.style.background='rgba(128,128,128,.25)';};
|
||
b.addEventListener('click',function(){
|
||
if(!A.supported)return;
|
||
try{window.chrome.webview.postMessage({type:type});}catch(e){}
|
||
});
|
||
return b;
|
||
}
|
||
fbtns.appendChild(mkBtn('Ordner wählen…',null,'sync-browse'));
|
||
fbtns.appendChild(mkBtn('Automatisch (OneDrive)','mystart-sync-folder-reset','sync-folder-reset'));
|
||
fr.appendChild(fpath);fr.appendChild(fbtns);row.appendChild(fr);
|
||
paint();
|
||
if(A.supported){
|
||
try{window.chrome.webview.postMessage({type:'autostart-get'});}catch(e){}
|
||
try{window.chrome.webview.postMessage({type:'sync-get'});}catch(e){}
|
||
}
|
||
}
|
||
function ensure(){
|
||
var row=document.getElementById('mystart-autostart-row');
|
||
if(row&&!document.getElementById('mystart-autostart-track'))render(row);
|
||
}
|
||
new MutationObserver(ensure).observe(document.documentElement,{childList:true,subtree:true});
|
||
ensure();
|
||
})();`;
|
||
|
||
// Frameless-window controls (minimize / maximize-restore / close) in the top-right
|
||
// corner. Only shown inside the app (WebView2 host); they postMessage to the host.
|
||
const windowControlsJs = `(function(){
|
||
if(!(window.chrome&&window.chrome.webview))return;
|
||
function send(t){try{window.chrome.webview.postMessage({type:t});}catch(e){}}
|
||
function mk(glyph,title,type,isClose){
|
||
var b=document.createElement('div');
|
||
b.textContent=glyph;b.title=title;
|
||
b.style.cssText='width:44px;height:30px;display:flex;align-items:center;justify-content:center;font-family:"Segoe UI",system-ui,sans-serif;font-size:14px;line-height:1;color:#d0d4db;cursor:pointer;transition:background .1s,color .1s;-webkit-user-select:none;user-select:none;';
|
||
b.onmouseenter=function(){b.style.background=isClose?'#e81123':'rgba(255,255,255,0.18)';b.style.color='#fff';};
|
||
b.onmouseleave=function(){b.style.background='transparent';b.style.color='#d0d4db';};
|
||
b.onclick=function(){send(type);};
|
||
return b;
|
||
}
|
||
function mkGrip(){
|
||
var g=document.createElement('div');
|
||
g.title='Fenster verschieben (ziehen) — oder Alt + ziehen';
|
||
g.style.cssText='width:34px;height:30px;display:flex;align-items:center;justify-content:center;color:#9aa0aa;cursor:move;-webkit-user-select:none;user-select:none;';
|
||
g.innerHTML='<svg width="10" height="16" viewBox="0 0 10 16" fill="currentColor"><circle cx="2" cy="2" r="1.4"/><circle cx="8" cy="2" r="1.4"/><circle cx="2" cy="8" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="2" cy="14" r="1.4"/><circle cx="8" cy="14" r="1.4"/></svg>';
|
||
g.onmouseenter=function(){g.style.color='#fff';};
|
||
g.onmouseleave=function(){g.style.color='#9aa0aa';};
|
||
g.addEventListener('mousedown',function(ev){if(ev.button!==0)return;ev.preventDefault();send('window-drag');});
|
||
return g;
|
||
}
|
||
function add(){
|
||
if(!document.body||document.getElementById('mystart-winctrls'))return;
|
||
var bar=document.createElement('div');
|
||
bar.id='mystart-winctrls';
|
||
bar.style.cssText='position:fixed;top:0;right:0;display:flex;align-items:center;z-index:2147483647;';
|
||
bar.appendChild(mkGrip());
|
||
bar.appendChild(mk('\\u2013','Minimieren','window-min',false));
|
||
bar.appendChild(mk('\\u25A1','Maximieren / Wiederherstellen','window-max',false));
|
||
bar.appendChild(mk('\\u2715','Schließen','window-close',true));
|
||
document.body.appendChild(bar);
|
||
}
|
||
if(document.body)add();else document.addEventListener('DOMContentLoaded',add);
|
||
new MutationObserver(function(){if(!document.getElementById('mystart-winctrls'))add();}).observe(document.documentElement,{childList:true,subtree:true});
|
||
// Alt + left-drag anywhere -> move the window (non-blocking).
|
||
document.addEventListener('mousedown',function(ev){
|
||
if(ev.button===0&&ev.altKey){ev.preventDefault();send('window-drag');}
|
||
},true);
|
||
// Edge/corner resize: the WebView fills the whole window, so the OS never
|
||
// sees the real window border to hit-test for a resize drag -- every mouse
|
||
// event lands in the page first. Watch the pointer ourselves near the
|
||
// viewport edges and hand off to the native resize loop, same trick as
|
||
// the drag grip above.
|
||
var RM=6; // px from the true edge that counts as a resize zone
|
||
var CURSOR={n:'ns-resize',s:'ns-resize',e:'ew-resize',w:'ew-resize',
|
||
ne:'nesw-resize',sw:'nesw-resize',nw:'nwse-resize',se:'nwse-resize'};
|
||
function edgeAt(x,y){
|
||
var w=window.innerWidth,h=window.innerHeight;
|
||
var l=x<=RM,r=x>=w-RM,t=y<=RM,b=y>=h-RM;
|
||
if(t&&l)return'nw';if(t&&r)return'ne';if(b&&l)return'sw';if(b&&r)return'se';
|
||
if(l)return'w';if(r)return'e';if(t)return'n';if(b)return's';
|
||
return null;
|
||
}
|
||
document.addEventListener('mousemove',function(ev){
|
||
if(ev.buttons)return; // an active drag (e.g. scrollbar) is in progress -> leave it alone
|
||
var onCtrls=ev.target.closest&&ev.target.closest('#mystart-winctrls,#mystart-profiles,#mystart-sync-toast');
|
||
var edge=onCtrls?null:edgeAt(ev.clientX,ev.clientY);
|
||
document.documentElement.style.cursor=edge?CURSOR[edge]:'';
|
||
},true);
|
||
document.addEventListener('mousedown',function(ev){
|
||
if(ev.button!==0)return;
|
||
if(ev.target.closest&&ev.target.closest('#mystart-winctrls,#mystart-profiles,#mystart-sync-toast'))return;
|
||
var edge=edgeAt(ev.clientX,ev.clientY);
|
||
if(edge){ev.preventDefault();send2('window-resize',{edge:edge});}
|
||
},true);
|
||
document.addEventListener('mouseleave',function(){document.documentElement.style.cursor='';},true);
|
||
function send2(t,extra){try{window.chrome.webview.postMessage(Object.assign({type:t},extra));}catch(e){}}
|
||
})();`;
|
||
|
||
// Optional, self-named data "Reiter" (tabs) in the top-left corner -- e.g.
|
||
// one for the office, one for a second workplace. Opt-in: with none created,
|
||
// only a small unobtrusive "+" shows -- nothing changes for anyone who
|
||
// doesn't want this. nightTab keeps its ENTIRE state (bookmarks, groups,
|
||
// theme, layout, ...) in a single localStorage key (the app name, here
|
||
// "MyStart"); switching swaps that key's content with a saved per-tab slot
|
||
// and reloads. Purely local to this install, no server/sync involved.
|
||
const profilesJs = `(function(){
|
||
var KEY='${APPNAME}';
|
||
var ACTIVE=KEY+'-slot-active';
|
||
var LIST=KEY+'-profiles';
|
||
function slotKey(id){return KEY+'-slot-'+id;}
|
||
function getProfiles(){try{var a=JSON.parse(localStorage.getItem(LIST));return Array.isArray(a)?a:[];}catch(e){return[];}}
|
||
function saveProfiles(a){localStorage.setItem(LIST,JSON.stringify(a));}
|
||
function getActive(){return localStorage.getItem(ACTIVE);}
|
||
function newId(){return 'p'+Date.now().toString(36)+Math.random().toString(36).slice(2,6);}
|
||
// Users of the earlier fixed "Büro"/"Extern" build had an old-format
|
||
// marker (ACTIVE='a' or 'b', no profiles list). Turn that into the new
|
||
// list format once, in place, without moving any data around.
|
||
function migrateLegacy(){
|
||
if(localStorage.getItem(LIST)!=null)return;
|
||
var legacyActive=localStorage.getItem(ACTIVE);
|
||
if(legacyActive==null)return;
|
||
var profiles=[];
|
||
['a','b'].forEach(function(s){
|
||
if(localStorage.getItem(slotKey(s))!=null||legacyActive===s){
|
||
var label=localStorage.getItem(KEY+'-slot-'+s+'-label')||(s==='a'?'Büro':'Extern');
|
||
profiles.push({id:s,label:label});
|
||
}
|
||
});
|
||
if(profiles.length)saveProfiles(profiles);else localStorage.removeItem(ACTIVE);
|
||
}
|
||
function loadDataInto(json){
|
||
if(json!=null){
|
||
localStorage.setItem(KEY,json);
|
||
// Pre-set the flash-prevention theme flag so reload doesn't briefly
|
||
// flash the previous tab's background colour.
|
||
try{
|
||
var style=JSON.parse(json).state.theme.style;
|
||
if(style==='dark'||style==='light')localStorage.setItem(KEY+'Style',style);
|
||
else localStorage.removeItem(KEY+'Style');
|
||
}catch(e){}
|
||
}else{
|
||
localStorage.removeItem(KEY);
|
||
localStorage.removeItem(KEY+'Style');
|
||
}
|
||
}
|
||
function switchTo(id){
|
||
var active=getActive();
|
||
if(id===active)return;
|
||
if(active!=null){
|
||
var live=localStorage.getItem(KEY);
|
||
if(live!=null)localStorage.setItem(slotKey(active),live);else localStorage.removeItem(slotKey(active));
|
||
}
|
||
loadDataInto(localStorage.getItem(slotKey(id)));
|
||
localStorage.setItem(ACTIVE,id);
|
||
location.reload();
|
||
}
|
||
function addProfile(){
|
||
var name=window.prompt('Name für den neuen Reiter:','');
|
||
if(!name||!name.trim())return;
|
||
name=name.trim();
|
||
var profiles=getProfiles();
|
||
var id=newId();
|
||
if(profiles.length===0&&getActive()==null){
|
||
// First tab ever: adopt whatever is currently loaded, nothing lost, no reload needed.
|
||
var current=localStorage.getItem(KEY);
|
||
if(current!=null)localStorage.setItem(slotKey(id),current);
|
||
localStorage.setItem(ACTIVE,id);
|
||
profiles.push({id:id,label:name});
|
||
saveProfiles(profiles);
|
||
render();
|
||
}else{
|
||
// Additional tab: stash the current one, start this one blank.
|
||
var active=getActive();
|
||
if(active!=null){
|
||
var live=localStorage.getItem(KEY);
|
||
if(live!=null)localStorage.setItem(slotKey(active),live);else localStorage.removeItem(slotKey(active));
|
||
}
|
||
loadDataInto(null);
|
||
profiles.push({id:id,label:name});
|
||
saveProfiles(profiles);
|
||
localStorage.setItem(ACTIVE,id);
|
||
location.reload();
|
||
}
|
||
}
|
||
function renameProfile(id){
|
||
var profiles=getProfiles();
|
||
var p=profiles.find(function(x){return x.id===id;});
|
||
if(!p)return;
|
||
var name=window.prompt('Name für diesen Reiter:',p.label);
|
||
if(name&&name.trim()){p.label=name.trim();saveProfiles(profiles);render();}
|
||
}
|
||
function deleteProfile(id){
|
||
var profiles=getProfiles();
|
||
var p=profiles.find(function(x){return x.id===id;});
|
||
if(!p)return;
|
||
if(!window.confirm('"'+p.label+'" wirklich entfernen? Die darin gespeicherten Daten dieses Reiters gehen dabei verloren.'))return;
|
||
profiles=profiles.filter(function(x){return x.id!==id;});
|
||
localStorage.removeItem(slotKey(id));
|
||
var wasActive=getActive()===id;
|
||
saveProfiles(profiles);
|
||
if(wasActive){
|
||
if(profiles.length){
|
||
var next=profiles[0].id;
|
||
loadDataInto(localStorage.getItem(slotKey(next)));
|
||
localStorage.setItem(ACTIVE,next);
|
||
location.reload();
|
||
}else{
|
||
// No tabs left -> back to a single, un-tabbed dataset (whatever was loaded stays, nothing wiped).
|
||
localStorage.removeItem(ACTIVE);
|
||
render();
|
||
}
|
||
}else{
|
||
render();
|
||
}
|
||
}
|
||
// --- Cloud sync hooks, called from the native host (Program.cs), which
|
||
// polls an OneDrive-synced JSON file. The host treats the exported blob as
|
||
// opaque; only this page understands/produces/consumes its structure. ---
|
||
window.__mystartSyncExport=function(){
|
||
var profiles=getProfiles();
|
||
var active=getActive();
|
||
var slots={};
|
||
profiles.forEach(function(p){
|
||
var raw=(p.id===active)?localStorage.getItem(KEY):localStorage.getItem(slotKey(p.id));
|
||
if(raw!=null){try{slots[p.id]=JSON.parse(raw);}catch(e){}}
|
||
});
|
||
var live=null;
|
||
if(active==null){
|
||
var liveRaw=localStorage.getItem(KEY);
|
||
if(liveRaw!=null){try{live=JSON.parse(liveRaw);}catch(e){}}
|
||
}
|
||
var data={profiles:profiles,active:active,slots:slots,live:live};
|
||
return JSON.stringify({data:data,savedAt:new Date().toISOString()});
|
||
};
|
||
window.__mystartSyncImport=function(json){
|
||
try{
|
||
var payload=JSON.parse(json);
|
||
var data=payload.data;
|
||
if(data.active==null){
|
||
if(data.live!=null)localStorage.setItem(KEY,JSON.stringify(data.live));else localStorage.removeItem(KEY);
|
||
localStorage.removeItem(LIST);
|
||
localStorage.removeItem(ACTIVE);
|
||
}else{
|
||
saveProfiles(data.profiles||[]);
|
||
localStorage.setItem(ACTIVE,data.active);
|
||
(data.profiles||[]).forEach(function(p){
|
||
var val=data.slots?data.slots[p.id]:null;
|
||
if(val!=null)localStorage.setItem(slotKey(p.id),JSON.stringify(val));
|
||
});
|
||
var activeVal=data.slots?data.slots[data.active]:null;
|
||
loadDataInto(activeVal!=null?JSON.stringify(activeVal):null);
|
||
}
|
||
location.reload();
|
||
return true;
|
||
}catch(e){return false;}
|
||
};
|
||
var syncDot;
|
||
var lastSync=null; // last status from the host, so a re-rendered dot isn't blank
|
||
function paintSyncDot(){
|
||
if(!syncDot)return;
|
||
// Sync is opt-in: no dot at all until it's switched on (Einstellungen -> MyStart).
|
||
if(!lastSync||lastSync.enabled===false){syncDot.style.display='none';return;}
|
||
syncDot.style.display='';
|
||
syncDot.style.background=lastSync.conflict?'#a371f7':(lastSync.ok?'#2ea043':'#e8b923');
|
||
var prefix=lastSync.conflict?'OneDrive-Sync (Konflikt): ':(lastSync.ok?'OneDrive-Sync: ':'OneDrive-Sync-Problem: ');
|
||
syncDot.title=prefix+lastSync.message;
|
||
}
|
||
function mkSyncDot(){
|
||
var d=document.createElement('div');
|
||
d.style.cssText='width:8px;height:8px;border-radius:50%;background:#565d68;margin-left:6px;flex:0 0 auto;';
|
||
syncDot=d;
|
||
paintSyncDot();
|
||
return d;
|
||
}
|
||
// Short-lived banner for a sync conflict -- a colour-only dot is too easy to
|
||
// miss for "you might have lost an edit just now".
|
||
var conflictToast=null,conflictToastTimer=null;
|
||
function showConflictToast(msg){
|
||
if(!conflictToast){
|
||
conflictToast=document.createElement('div');
|
||
conflictToast.id='mystart-sync-toast';
|
||
conflictToast.style.cssText='position:fixed;top:44px;left:6px;max-width:360px;padding:8px 12px;'+
|
||
'border-radius:8px;background:rgba(60,30,90,.92);color:#f0e6ff;font-size:12px;line-height:1.4;'+
|
||
'box-shadow:0 2px 10px rgba(0,0,0,.4);z-index:2147483647;cursor:pointer;';
|
||
conflictToast.title='Zum Ausblenden klicken';
|
||
conflictToast.addEventListener('click',hideConflictToast);
|
||
document.body.appendChild(conflictToast);
|
||
}
|
||
conflictToast.textContent='⚠ '+msg;
|
||
conflictToast.style.display='';
|
||
if(conflictToastTimer)clearTimeout(conflictToastTimer);
|
||
conflictToastTimer=setTimeout(hideConflictToast,10000);
|
||
}
|
||
function hideConflictToast(){
|
||
if(conflictToast)conflictToast.style.display='none';
|
||
if(conflictToastTimer){clearTimeout(conflictToastTimer);conflictToastTimer=null;}
|
||
}
|
||
if(window.chrome&&window.chrome.webview){
|
||
window.chrome.webview.addEventListener('message',function(ev){
|
||
var d=ev.data;
|
||
if(!d||d.type!=='sync-status')return;
|
||
var wasConflict=lastSync&&lastSync.conflict;
|
||
lastSync=d;
|
||
paintSyncDot();
|
||
if(d.conflict&&!wasConflict)showConflictToast('Gleichzeitige Änderung erkannt — dein Stand wurde trotzdem gespeichert: '+d.message);
|
||
});
|
||
}
|
||
var bar;
|
||
var TXT_SHADOW='text-shadow:0 1px 3px rgba(0,0,0,.85),0 0 1px rgba(0,0,0,.6);';
|
||
function mkPlus(){
|
||
var b=document.createElement('div');
|
||
b.textContent='+';
|
||
b.title='Neuen Reiter anlegen';
|
||
b.style.cssText='width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"Segoe UI",system-ui,sans-serif;font-size:15px;font-weight:600;color:#c7cbd3;cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:6px;transition:background .12s,color .12s;'+TXT_SHADOW;
|
||
b.onmouseenter=function(){b.style.background='rgba(255,255,255,0.16)';b.style.color='#fff';};
|
||
b.onmouseleave=function(){b.style.background='transparent';b.style.color='#c7cbd3';};
|
||
b.addEventListener('click',function(){addProfile();});
|
||
return b;
|
||
}
|
||
function mkTab(p,isActive){
|
||
var wrap=document.createElement('div');
|
||
wrap.style.cssText='position:relative;display:flex;align-items:center;';
|
||
var b=document.createElement('div');
|
||
b.textContent=p.label;
|
||
b.title='Zu diesem Reiter wechseln — Doppelklick zum Umbenennen';
|
||
b.style.cssText='padding:6px 22px 6px 12px;font-family:"Segoe UI",system-ui,sans-serif;font-size:12.5px;cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:6px;transition:background .12s,color .12s;'+TXT_SHADOW+
|
||
(isActive
|
||
? 'color:#ffffff;font-weight:700;background:rgba(255,255,255,0.22);box-shadow:inset 0 0 0 1px rgba(255,255,255,0.28);'
|
||
: 'color:#d7dae0;font-weight:500;background:transparent;');
|
||
b.addEventListener('click',function(){switchTo(p.id);});
|
||
b.addEventListener('dblclick',function(ev){ev.preventDefault();renameProfile(p.id);});
|
||
var x=document.createElement('div');
|
||
x.textContent='\\u00d7';
|
||
x.title='Reiter entfernen';
|
||
x.style.cssText='position:absolute;right:2px;top:50%;transform:translateY(-50%);width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:12px;color:#c7cbd3;cursor:pointer;opacity:0;transition:opacity .12s,color .12s,background .12s;border-radius:4px;';
|
||
x.addEventListener('click',function(ev){ev.stopPropagation();deleteProfile(p.id);});
|
||
// Toggle on the wrapper (not the label itself) so moving the cursor
|
||
// from the label onto the x doesn't count as "leaving" and hide it again mid-click.
|
||
wrap.addEventListener('mouseenter',function(){if(!isActive)b.style.background='rgba(255,255,255,0.14)';x.style.opacity='1';});
|
||
wrap.addEventListener('mouseleave',function(){if(!isActive)b.style.background='transparent';x.style.opacity='0';});
|
||
x.onmouseenter=function(){x.style.color='#fff';x.style.background='#e81123';};
|
||
x.onmouseleave=function(){x.style.color='#c7cbd3';x.style.background='transparent';};
|
||
wrap.appendChild(b);wrap.appendChild(x);
|
||
return wrap;
|
||
}
|
||
function render(){
|
||
if(!bar)return;
|
||
bar.innerHTML='';
|
||
var active=getActive();
|
||
getProfiles().forEach(function(p){bar.appendChild(mkTab(p,p.id===active));});
|
||
bar.appendChild(mkPlus());
|
||
bar.appendChild(mkSyncDot());
|
||
}
|
||
function add(){
|
||
if(!document.body||document.getElementById('mystart-profiles'))return;
|
||
migrateLegacy();
|
||
bar=document.createElement('div');
|
||
bar.id='mystart-profiles';
|
||
bar.style.cssText='position:fixed;top:6px;left:6px;display:flex;align-items:center;gap:2px;padding:4px;z-index:2147483647;background:rgba(22,24,29,0.68);border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.4),inset 0 0 0 1px rgba(255,255,255,0.06);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);';
|
||
document.body.appendChild(bar);
|
||
render();
|
||
}
|
||
if(document.body)add();else document.addEventListener('DOMContentLoaded',add);
|
||
new MutationObserver(function(){if(!document.getElementById('mystart-profiles'))add();}).observe(document.documentElement,{childList:true,subtree:true});
|
||
})();`;
|
||
|
||
// Local images/videos from this PC in the image/video URL fields (kept as its
|
||
// own file: it is full of backslash regexes that are painful inside a template string).
|
||
const localFilesJs = fs.readFileSync(path.join(__dirname, 'inject', 'local-files.js'), 'utf8');
|
||
|
||
mainJs = trInfra + autostartJs + windowControlsJs + profilesJs + localFilesJs + mainJs;
|
||
|
||
const trPatches = [
|
||
// panel header (top-level AND sub, both go through element.header)
|
||
['y("h1:"+e+"|class:menu-item-header-text")',
|
||
'y("h1:"+window.__TR(e)+"|class:menu-item-header-text")'],
|
||
// top-level nav tab label
|
||
['new Fe({text:e.name,style:["link"],block:!0,classList:["menu-nav-tab"]',
|
||
'new Fe({text:window.__TR(e.name),style:["link"],block:!0,classList:["menu-nav-tab"]'],
|
||
// sub-level nav label (href keeps makeId -> English routing key)
|
||
['y("a:"+e+"|href:#menu-content-item-"+this.makeId(e)',
|
||
'y("a:"+window.__TR(e)+"|href:#menu-content-item-"+this.makeId(e)'],
|
||
];
|
||
for (const [from, to] of trPatches) {
|
||
if (!mainJs.includes(from)) throw new Error('translate patch not found: ' + from);
|
||
mainJs = mainJs.replace(from, to);
|
||
}
|
||
|
||
// --- 5) Translate safe, unique display strings (search box, greeting, key texts).
|
||
// Tolerant: each is applied only if the exact literal is present. Structural
|
||
// names and icon labels are deliberately NOT touched.
|
||
const dict = [
|
||
// search box placeholder (base + engine name is appended after it)
|
||
['?"Find bookmarks or search":"Search"', '?"Lesezeichen finden oder suchen mit":"Suchen mit"'],
|
||
// greeting words
|
||
['"Good morning"', '"Guten Morgen"'],
|
||
['"Good afternoon"', '"Guten Tag"'],
|
||
['"Good evening"', '"Guten Abend"'],
|
||
['"Good night"', '"Gute Nacht"'],
|
||
['"Hello"', '"Hallo"'],
|
||
['"Welcome"', '"Willkommen"'],
|
||
['"Greetings"', '"Grüße"'],
|
||
// Data panel / confirmation descriptions (post-rename -> contain MyStart)
|
||
['"Restore a previously exported MyStart backup."', '"Eine zuvor exportierte MyStart-Sicherung wiederherstellen."'],
|
||
['"Restoring from a MyStart backup"', '"Aus einer MyStart-Sicherung wiederherstellen"'],
|
||
['"Clear all data to reset MyStart to the default state."', '"Alle Daten löschen, um MyStart auf den Ausgangszustand zurückzusetzen."'],
|
||
['"Clear MyStart data except bookmarks?"', '"MyStart-Daten außer Lesezeichen löschen?"'],
|
||
['"Or drop a MyStart backup file here."', '"Oder ziehe eine MyStart-Sicherungsdatei hierher."'],
|
||
// universal button
|
||
['text:"Add"', 'text:"Hinzufügen"'],
|
||
];
|
||
for (const [from, to] of dict) {
|
||
if (mainJs.includes(from)) mainJs = mainJs.split(from).join(to);
|
||
}
|
||
|
||
// --- 6) Full German UI: translate every remaining visible control/label/tooltip.
|
||
// Applied ONLY to value positions (:"...") so routing names (name:"Theme",
|
||
// sub:["Style"], case"layout") and the icon database stay untouched. The button
|
||
// behaviour lives in func()/keys, never in the display text, so this is safe.
|
||
const chrome = Object.entries(JSON.parse(fs.readFileSync(path.join(__dirname, 'lang', 'de.json'), 'utf8')));
|
||
const _unusedChrome = [
|
||
['Add', 'Hinzufügen'],
|
||
['Add 10 random bookmarks', '10 zufällige Lesezeichen hinzufügen'],
|
||
['Add a group', 'Gruppe hinzufügen'],
|
||
['Add a new Bookmark', 'Neues Lesezeichen hinzufügen'],
|
||
['Add a new Group', 'Neue Gruppe hinzufügen'],
|
||
['Add unique accent to each Bookmark', 'Jedem Lesezeichen eigene Akzentfarbe geben'],
|
||
['Adjust Visual Element position', 'Position des Symbols anpassen'],
|
||
['Area alignment', 'Ausrichtung des Bereichs'],
|
||
['Bold', 'Fett'],
|
||
['Bookmark area alignment', 'Ausrichtung des Lesezeichen-Bereichs'],
|
||
['Bookmark area background colour', 'Hintergrundfarbe des Lesezeichen-Bereichs'],
|
||
['By icon', 'Nach Icon'],
|
||
['By letter', 'Nach Buchstabe'],
|
||
['By name', 'Nach Name'],
|
||
['Clear all data', 'Alle Daten löschen'],
|
||
['Clear all except bookmarks', 'Alles außer Lesezeichen löschen'],
|
||
['Clear all MyStart data?', 'Alle MyStart-Daten löschen?'],
|
||
['Clear search', 'Suche leeren'],
|
||
['Close settings menu', 'Einstellungen schließen'],
|
||
['Collapse this Group', 'Diese Gruppe einklappen'],
|
||
['Copy to clipboard', 'In die Zwischenablage kopieren'],
|
||
['Drag bookmark to reorder', 'Lesezeichen ziehen zum Umsortieren'],
|
||
['Drag group to reorder', 'Gruppe ziehen zum Umsortieren'],
|
||
['Drag header item to reorder', 'Kopfzeilen-Element ziehen zum Umsortieren'],
|
||
['Edit saved themes', 'Gespeicherte Designs bearbeiten'],
|
||
['Edit this bookmark', 'Dieses Lesezeichen bearbeiten'],
|
||
['Edit this group', 'Diese Gruppe bearbeiten'],
|
||
['Edit this saved theme', 'Dieses gespeicherte Design bearbeiten'],
|
||
['Edit unnamed bookmark', 'Unbenanntes Lesezeichen bearbeiten'],
|
||
['Edit unnamed group', 'Unbenannte Gruppe bearbeiten'],
|
||
['Enter edit bookmark mode', 'Bearbeitungsmodus für Lesezeichen'],
|
||
['Export data', 'Daten exportieren'],
|
||
['Google Font', 'Google-Schriftart'],
|
||
['Google font name', 'Name der Google-Schriftart'],
|
||
['Group details area alignment', 'Ausrichtung des Gruppen-Detailbereichs'],
|
||
['Header area alignment', 'Ausrichtung des Kopfzeilen-Bereichs'],
|
||
['Header background colour', 'Hintergrundfarbe der Kopfzeile'],
|
||
['Header item alignment', 'Ausrichtung der Kopfzeilen-Elemente'],
|
||
['Hide all', 'Alle ausblenden'],
|
||
['How to link to a video file.', 'So verlinkst du eine Videodatei.'],
|
||
['Import', 'Importieren'],
|
||
['Import from clipboard', 'Aus Zwischenablage importieren'],
|
||
['Large button', 'Große Schaltfläche'],
|
||
['Layout Direction', 'Layout-Richtung'],
|
||
['Layout background colour', 'Hintergrundfarbe des Layouts'],
|
||
['Light', 'Dünn'],
|
||
['Line button', 'Linien-Schaltfläche'],
|
||
['Link button', 'Link-Schaltfläche'],
|
||
['Medium button', 'Mittlere Schaltfläche'],
|
||
['More controls', 'Mehr Optionen'],
|
||
['Move this bookmark left', 'Dieses Lesezeichen nach links'],
|
||
['Move this bookmark right', 'Dieses Lesezeichen nach rechts'],
|
||
['Move this group down', 'Diese Gruppe nach unten'],
|
||
['Move this group right', 'Diese Gruppe nach rechts'],
|
||
['Move this group up', 'Diese Gruppe nach oben'],
|
||
['Name hide', 'Name ausblenden'],
|
||
['Name show', 'Name anzeigen'],
|
||
['New Bookmark', 'Neues Lesezeichen'],
|
||
['New Group', 'Neue Gruppe'],
|
||
['New Tab', 'Neuer Tab'],
|
||
['Only icons', 'Nur Icons'],
|
||
['Only images', 'Nur Bilder'],
|
||
['Only letters', 'Nur Buchstaben'],
|
||
['Open all Bookmarks in this Group', 'Alle Lesezeichen dieser Gruppe öffnen'],
|
||
['Open settings menu', 'Einstellungen öffnen'],
|
||
['Random Group name', 'Zufälliger Gruppenname'],
|
||
['Random colour', 'Zufällige Farbe'],
|
||
['Random theme name', 'Zufälliger Designname'],
|
||
['Randomise now', 'Jetzt zufällig'],
|
||
['Regular', 'Normal'],
|
||
['Remove', 'Entfernen'],
|
||
['Remove all accent overrides', 'Alle Akzent-Überschreibungen entfernen'],
|
||
['Remove icon', 'Icon entfernen'],
|
||
['Remove this bookmark', 'Dieses Lesezeichen entfernen'],
|
||
['Remove this group', 'Diese Gruppe entfernen'],
|
||
['Remove this saved theme', 'Dieses gespeicherte Design entfernen'],
|
||
['Remove unnamed bookmark', 'Unbenanntes Lesezeichen entfernen'],
|
||
['Reset to default', 'Auf Standard zurücksetzen'],
|
||
['Ring button', 'Ring-Schaltfläche'],
|
||
['Save', 'Speichern'],
|
||
['Save current theme', 'Aktuelles Design speichern'],
|
||
['Scrollbar', 'Bildlaufleiste'],
|
||
['Search Bookmarks or', 'Lesezeichen durchsuchen oder'],
|
||
['Search box size', 'Größe des Suchfelds'],
|
||
['Search box width', 'Breite des Suchfelds'],
|
||
['Search engine', 'Suchmaschine'],
|
||
['Search engine name', 'Name der Suchmaschine'],
|
||
['Search text alignment', 'Textausrichtung der Suche'],
|
||
['Show all', 'Alle anzeigen'],
|
||
['Small button', 'Kleine Schaltfläche'],
|
||
['Start of the week', 'Wochenbeginn'],
|
||
['Toolbar position', 'Position der Werkzeugleiste'],
|
||
['Visual Element and Name alignment', 'Ausrichtung von Symbol und Name'],
|
||
['Why has this changed?', 'Warum hat sich das geändert?'],
|
||
['Word length', 'Wortlänge'],
|
||
['Wording', 'Formulierung'],
|
||
['You can restore all or parts of a backup file. The following data will be restored:',
|
||
'Du kannst eine Sicherung ganz oder teilweise wiederherstellen. Folgende Daten werden wiederhergestellt:'],
|
||
// placeholders
|
||
['Example', 'Beispiel'],
|
||
['Example group', 'Beispielgruppe'],
|
||
['Example theme', 'Beispiel-Design'],
|
||
['FontAwesome Brands or Icons', 'FontAwesome Brands oder Icons'],
|
||
['Hex code', 'Hex-Code'],
|
||
['Nickname, alias or superhero name', 'Spitzname, Alias oder Heldenname'],
|
||
];
|
||
// Locate the FontAwesome icon array (mr=[{name:"500px"...]) so we NEVER touch it.
|
||
const iconStart = mainJs.indexOf('mr=[{name:"500px"');
|
||
let iconEnd = -1;
|
||
if (iconStart >= 0) {
|
||
const bracket = mainJs.indexOf('[', iconStart);
|
||
let depth = 0;
|
||
for (let i = bracket; i < mainJs.length; i++) {
|
||
const c = mainJs[i];
|
||
if (c === '[') depth++;
|
||
else if (c === ']') { depth--; if (depth === 0) { iconEnd = i + 1; break; } }
|
||
else if (c === '"') { i++; while (i < mainJs.length && mainJs[i] !== '"') { if (mainJs[i] === '\\') i++; i++; } }
|
||
}
|
||
}
|
||
if (iconStart < 0 || iconEnd < 0) throw new Error('icon region not found — aborting translation');
|
||
|
||
const before = mainJs.slice(0, iconStart);
|
||
const iconRegion = mainJs.slice(iconStart, iconEnd);
|
||
const after = mainJs.slice(iconEnd);
|
||
|
||
// Only translate genuine DISPLAY properties, and only outside the icon data.
|
||
const displayProps = ['labelText', 'text', 'description', 'label', 'title', 'placeholder', 'heading', 'successText', 'tabText'];
|
||
const jsEsc = (v) => v.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||
const translateChrome = (s) => {
|
||
for (const [en, de] of chrome) {
|
||
const deEsc = jsEsc(de);
|
||
for (const p of displayProps) {
|
||
const from = p + ':"' + en + '"';
|
||
if (s.includes(from)) s = s.split(from).join(p + ':"' + deEsc + '"');
|
||
}
|
||
}
|
||
return s;
|
||
};
|
||
mainJs = translateChrome(before) + iconRegion + translateChrome(after);
|
||
|
||
// --- 7) Long help/feedback/description sentences (y-DSL `y("p:..")`, text:[..] arrays,
|
||
// and description:"..."). Safe via exact match because these strings are long & unique.
|
||
const de2 = Object.entries(JSON.parse(fs.readFileSync(path.join(__dirname, 'lang', 'de2.json'), 'utf8')));
|
||
for (const [en, de] of de2) {
|
||
const deEsc = jsEsc(de);
|
||
// quoted literal (arrays, description:"...", any "..." occurrence)
|
||
mainJs = mainJs.split('"' + en + '"').join('"' + deEsc + '"');
|
||
// y-DSL with trailing attributes: y("p:TEXT|class:..")
|
||
mainJs = mainJs.split(':' + en + '|').join(':' + deEsc + '|');
|
||
// y-DSL without attributes: y("p:TEXT")
|
||
mainJs = mainJs.split(':' + en + '"').join(':' + deEsc + '"');
|
||
}
|
||
|
||
// --- 8) Two font-field hints that contain escaped quotes in the source literal. ---
|
||
const fontHints = [
|
||
['Clear the field to use the default font "Fjalla One".', 'Feld leeren, um die Standardschrift "Fjalla One" zu verwenden.'],
|
||
['Clear the field to use the default font "Open Sans".', 'Feld leeren, um die Standardschrift "Open Sans" zu verwenden.'],
|
||
];
|
||
for (const [en, de] of fontHints) {
|
||
if (mainJs.includes(en)) mainJs = mainJs.split(en).join(de);
|
||
}
|
||
|
||
const favSvg = fs.readFileSync(path.join(root, 'icon/favicon.svg'));
|
||
const favData = 'data:image/svg+xml;base64,' + favSvg.toString('base64');
|
||
|
||
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"><title>${APPNAME}</title><link rel="icon" type="image/svg+xml" class="favicon" href="${favData}"/><style media="screen">html,body {background-color: rgb(0, 0, 0);}@media (prefers-color-scheme: dark) {html,body {background-color: rgb(0, 0, 0);}}@media (prefers-color-scheme: light) {html,body {background-color: rgb(255, 255, 255);}}</style>
|
||
<style>${css}</style>
|
||
<style>/* Scrollbalken ans dunkle MyStart-Design anpassen (schlank, dunkel, ohne Pfeile) */
|
||
::-webkit-scrollbar{width:10px;height:10px;}
|
||
::-webkit-scrollbar-track{background:transparent;}
|
||
::-webkit-scrollbar-thumb{background-color:rgba(255,255,255,0.22);border-radius:8px;border:2px solid transparent;background-clip:padding-box;}
|
||
::-webkit-scrollbar-thumb:hover{background-color:rgba(255,255,255,0.38);background-clip:padding-box;}
|
||
::-webkit-scrollbar-button{display:none;height:0;width:0;}
|
||
::-webkit-scrollbar-corner{background:transparent;}</style>
|
||
<script>${initJs}</script>
|
||
</head><body>
|
||
<script>${mainJs}</script>
|
||
</body></html>`;
|
||
|
||
fs.writeFileSync(out, html);
|
||
const kb = (fs.statSync(out).size / 1024).toFixed(0);
|
||
console.log('Written: ' + out + ' (' + kb + ' KB) [Support/Coffee removed, renamed to ' + APPNAME + ']');
|