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.1|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; function paint(){ var tr=document.getElementById('mystart-autostart-track'); var kn=document.getElementById('mystart-autostart-knob'); if(!tr||!kn)return; tr.style.background=A.enabled?'#2ea043':'#565d68'; kn.style.left=A.enabled?'23px':'3px'; } if(A.supported){ window.chrome.webview.addEventListener('message',function(ev){ var d=ev.data;if(d&&d.type==='autostart'){A.enabled=!!d.enabled;paint();} }); } function render(row){ var wrap=document.createElement('div'); 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='mystart-autostart-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='mystart-autostart-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='Mit Windows starten (minimiert)'; 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); paint(); wrap.addEventListener('click',function(){ if(!A.supported)return; A.enabled=!A.enabled;paint(); try{window.chrome.webview.postMessage({type:'autostart-set',value:A.enabled});}catch(e){} }); if(A.supported){try{window.chrome.webview.postMessage({type:'autostart-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=''; 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'); 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'))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){}} })();`; mainJs = trInfra + autostartJs + windowControlsJs + 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 = `