Files
MyStart/bundle.js
2026-08-06 07:18:14 +02:00

438 lines
23 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.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='<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');
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 = `<!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 + ']');