Files
MyStart/bundle.js
T
2026-09-22 18:59:48 +00:00

769 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 + ']');