Upload via GUI (50 Dateien)

This commit is contained in:
2026-09-29 07:09:26 +00:00
parent ee0e6dec32
commit d268e548f5
11 changed files with 1436 additions and 49 deletions
+355 -24
View File
@@ -1507,6 +1507,7 @@ const ICONS_SVG = {
folder: '<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>',
terminal: '<path d="M4 5h16v14H4zM7 9l3 3-3 3M13 16h4"/>',
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/>',
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>',
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>',
play: '<path d="M7 5l12 7-12 7z"/>',
blocks: '<path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/>',
@@ -2974,26 +2975,43 @@ function erzeugeSkaliertenStream(quellStream, zielBreite, zielHoehe, fps) {
// unsichtbare Rahmen für die Größenänderung per Maus) - sonst wäre am Rand
// ein dünner Streifen Desktop/Nachbarfenster im Zuschnitt zu sehen.
const UNSICHTBARER_RAND = 8;
const zeichnen = () => {
if (video.readyState >= 2) {
if (mcFensterBounds && mcBildschirmBounds && video.videoWidth && mcBildschirmBounds.width) {
const skalX = video.videoWidth / mcBildschirmBounds.width;
const skalY = video.videoHeight / mcBildschirmBounds.height;
const randX = UNSICHTBARER_RAND * skalX;
const randY = UNSICHTBARER_RAND * skalY;
const sx = (mcFensterBounds.x - mcBildschirmBounds.x) * skalX + randX;
const sy = (mcFensterBounds.y - mcBildschirmBounds.y) * skalY + randY;
const sw = Math.max(1, mcFensterBounds.width * skalX - randX * 2);
const sh = Math.max(1, mcFensterBounds.height * skalY - randY * 2);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, zielBreite, zielHoehe);
} else {
ctx.drawImage(video, 0, 0, zielBreite, zielHoehe); // Übergang, bis Fenstergrenzen bekannt sind
// captureStream(fps) allein reicht nicht: requestAnimationFrame läuft mit
// der Bildwiederholrate des Monitors (z. B. 144Hz), nicht mit der
// gewünschten Aufnahme-Bildrate - ungebremst wurden dadurch weit mehr
// Bilder gezeichnet/geliefert als angefragt (führte zu einer kaputten
// Zeitachse im aufgenommenen Video und sichtbarem Ruckeln). Deshalb
// captureStream(0) (kein automatisches Intervall) und Bilder nur exakt im
// Takt der Ziel-Bildrate selbst per requestFrame() ausliefern.
const canvasStream = canvas.captureStream(0);
const spurAusgabe = canvasStream.getVideoTracks()[0];
const intervallMs = 1000 / fps;
let letzteZeichnung = 0;
const zeichnen = (jetzt) => {
if (!letzteZeichnung || jetzt - letzteZeichnung >= intervallMs) {
letzteZeichnung = jetzt;
if (video.readyState >= 2) {
if (mcFensterBounds && mcBildschirmBounds && video.videoWidth && mcBildschirmBounds.width) {
const skalX = video.videoWidth / mcBildschirmBounds.width;
const skalY = video.videoHeight / mcBildschirmBounds.height;
const randX = UNSICHTBARER_RAND * skalX;
const randY = UNSICHTBARER_RAND * skalY;
const sx = (mcFensterBounds.x - mcBildschirmBounds.x) * skalX + randX;
const sy = (mcFensterBounds.y - mcBildschirmBounds.y) * skalY + randY;
const sw = Math.max(1, mcFensterBounds.width * skalX - randX * 2);
const sh = Math.max(1, mcFensterBounds.height * skalY - randY * 2);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, zielBreite, zielHoehe);
} else {
ctx.drawImage(video, 0, 0, zielBreite, zielHoehe); // Übergang, bis Fenstergrenzen bekannt sind
}
spurAusgabe.requestFrame();
}
}
mcCanvasRAF = requestAnimationFrame(zeichnen);
};
zeichnen();
return canvas.captureStream(fps);
mcCanvasRAF = requestAnimationFrame(zeichnen);
return canvasStream;
}
async function erfasseFensterVideoMitCursor(source, grenzen) {
@@ -3039,9 +3057,8 @@ async function wendeVideoEinstellungenAn() {
function updateVideoSummary() {
const groesse = mcActualSize ? ` (${mcActualSize.width}×${mcActualSize.height})` : '';
const cursorHinweis = mcVideoStream ? (mcCursorAusgeblendet ? ' · ohne Mauszeiger' : ' · mit Mauszeiger') : '';
el('record-video-summary').textContent =
RESOLUTION_LABEL[mcResolution] + groesse + ' · ' + QUALITAET_LABEL[mcQuality] + ' · ' + mcFramerate + ' fps' + cursorHinweis;
RESOLUTION_LABEL[mcResolution] + groesse + ' · ' + QUALITAET_LABEL[mcQuality] + ' · ' + mcFramerate + ' fps';
const aktiv = recordingActive();
el('record-resolution').disabled = aktiv;
el('record-quality').disabled = aktiv;
@@ -3290,7 +3307,6 @@ async function onMcRecordingStopped() {
clearInterval(mcTimerInterval);
window.api.recordingSetGuard(false);
el('record-live-badge').classList.add('hidden');
el('record-toggle').textContent = 'Aufnahme starten';
el('record-timer').textContent = '00:00';
updateVideoSummary();
cleanupAudioMix();
@@ -3301,7 +3317,16 @@ async function onMcRecordingStopped() {
const buffer = await blob.arrayBuffer();
const inst = await window.api.getInstance(currentDetailId);
mcLastSavedInstanceName = inst ? inst.name : 'Instanz';
const res = await window.api.recordingSave(mcLastSavedInstanceName, buffer, await aktuellerAufnahmeServer());
// Die Umwandlung nach MP4 (für Abspielbarkeit in jedem Player) läuft nicht
// sofort durch - kurz sichtbar machen, damit der Knopf währenddessen nicht
// wie eingefroren wirkt
const knopf = el('record-toggle');
knopf.disabled = true;
knopf.textContent = 'Wird verarbeitet…';
const res = await window.api.recordingSave(mcLastSavedInstanceName, buffer, await aktuellerAufnahmeServer(), mcFramerate);
knopf.disabled = false;
knopf.textContent = 'Aufnahme starten';
if (res.ok) {
toast('Aufzeichnung gespeichert.');
} else {
@@ -3338,6 +3363,8 @@ async function openScreens() {
grid.innerHTML = '<div class="screens-empty">Lade …</div>';
screenSelected = new Set();
updateScreensSelectionUi();
recordsGeladen = false;
switchScreensTab('photos');
show('modal-screens');
const shots = await window.api.screenshots(currentDetailId);
@@ -3402,6 +3429,251 @@ async function deleteSelectedScreenshots() {
});
}
// ---------- Aufzeichnungen-Galerie (im selben Dialog, zweiter Tab) ----------
let screensAktiverTab = 'photos';
let recordSelected = new Set();
let recordsGeladen = false; // nur beim ersten Wechsel auf den Tab laden, nicht bei jedem Öffnen
function switchScreensTab(which) {
screensAktiverTab = which;
const photos = which === 'photos';
el('screens-tab-photos').classList.toggle('on', photos);
el('screens-tab-records').classList.toggle('on', !photos);
el('screens-pane-photos').classList.toggle('hidden', !photos);
el('screens-pane-records').classList.toggle('hidden', photos);
if (!photos && !recordsGeladen) { recordsGeladen = true; openRecordsList(); }
}
function updateRecordsSelectionUi() {
const n = recordSelected.size;
const btn = el('records-delete-sel');
if (btn) {
btn.disabled = n === 0;
btn.textContent = n ? (n + ' löschen') : 'Auswahl löschen';
}
const hint = el('records-sel-hint');
if (hint) hint.textContent = n ? (n + ' ausgewählt – Doppelklick spielt ab') : 'Klick auf Vorschaubild zum Auswählen, Doppelklick zum Abspielen';
document.querySelectorAll('#records-grid .screen-thumb').forEach((t) => {
const name = t.dataset.name;
t.classList.toggle('selected', !!(name && recordSelected.has(name)));
const cb = t.querySelector('.screen-check');
if (cb) cb.checked = !!(name && recordSelected.has(name));
});
}
function formatDateigroesse(bytes) {
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return Math.round(bytes / 1024) + ' KB';
}
async function openRecordsList() {
const grid = el('records-grid');
grid.innerHTML = '<div class="screens-empty">Lade …</div>';
recordSelected = new Set();
updateRecordsSelectionUi();
const inst = await window.api.getInstance(currentDetailId);
const instanceName = inst ? inst.name : null;
if (!instanceName) { grid.innerHTML = '<div class="screens-empty">Keine Instanz ausgewählt.</div>'; return; }
const aufnahmen = await window.api.recordingList(instanceName);
grid.innerHTML = '';
if (!aufnahmen.length) {
grid.innerHTML = '<div class="screens-empty">Keine Aufzeichnungen vorhanden.</div>';
updateRecordsSelectionUi();
return;
}
for (const a of aufnahmen) {
const thumb = document.createElement('div');
thumb.className = 'screen-thumb record-thumb';
thumb.dataset.name = a.name;
thumb.innerHTML =
`<input type="checkbox" class="screen-check" title="Auswählen" />
<div class="record-thumb-img-wrap"><img alt="" /><span class="record-thumb-play">▶</span></div>
<div class="screen-name" title="${escapeHtml(a.name)}">${escapeHtml(a.name)}</div>
<div class="record-thumb-size">${formatDateigroesse(a.size)}</div>
<button class="screen-del" title="Löschen">🗑</button>`;
const img = thumb.querySelector('img');
window.api.recordingThumbnail(instanceName, a.name).then((d) => { if (d) img.src = d; });
const toggle = () => {
if (recordSelected.has(a.name)) recordSelected.delete(a.name);
else recordSelected.add(a.name);
updateRecordsSelectionUi();
};
thumb.querySelector('.screen-check').addEventListener('click', (e) => { e.stopPropagation(); toggle(); });
thumb.addEventListener('click', (e) => {
if (e.target.closest('.screen-del') || e.target.closest('.screen-check')) return;
toggle();
});
thumb.addEventListener('dblclick', (e) => {
if (e.target.closest('.screen-del') || e.target.closest('.screen-check')) return;
window.api.recordingOpen(instanceName, a.name);
});
thumb.querySelector('.screen-del').addEventListener('click', (e) => {
e.stopPropagation();
askConfirm('Aufzeichnung löschen', `„${a.name}" löschen?`, '🗑 Löschen', async () => {
await window.api.recordingDelete(instanceName, a.name);
openRecordsList();
});
});
grid.appendChild(thumb);
}
updateRecordsSelectionUi();
}
async function deleteSelectedRecords() {
const names = [...recordSelected];
if (!names.length || !currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
const instanceName = inst ? inst.name : null;
if (!instanceName) return;
const label = names.length === 1
? `„${names[0]}" löschen?`
: `${names.length} Aufzeichnungen unwiderruflich löschen?`;
askConfirm('Aufzeichnungen löschen', label, '🗑 Löschen', async () => {
for (const name of names) {
await window.api.recordingDelete(instanceName, name);
}
toast(names.length === 1 ? 'Aufzeichnung gelöscht.' : names.length + ' Aufzeichnungen gelöscht.');
openRecordsList();
});
}
// ---------- Freunde ----------
let friendsAutoRefresh = null;
async function openFriends() {
if (POPOUT !== 'friends') { window.api.popoutOpen('friends'); return; }
show('modal-friends');
await ladeFreunde();
clearInterval(friendsAutoRefresh);
friendsAutoRefresh = setInterval(ladeFreunde, 30000);
}
async function ladeFreunde() {
const aktiv = await window.api.friendsEnabled();
el('friends-disabled-hint').classList.toggle('hidden', aktiv);
el('friends-content').classList.toggle('hidden', !aktiv);
if (!aktiv) return;
const [anfragenRes, gesendetRes, freundeRes, einstellungen] = await Promise.all([
window.api.friendsRequests(),
window.api.friendsSentRequests(),
window.api.friendsList(),
window.api.getSettings(),
]);
const spitznamen = einstellungen.friendNicknames || {};
const reqBox = el('friends-requests-box');
reqBox.innerHTML = '';
if (anfragenRes.ok && anfragenRes.list.length) {
const titel = document.createElement('div');
titel.className = 'friends-section-title';
titel.textContent = 'Anfragen';
reqBox.appendChild(titel);
for (const r of anfragenRes.list) {
const row = document.createElement('div');
row.className = 'friend-row';
row.innerHTML = `<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(r.uuid)}/64" alt="" />
<div class="friend-info"><div class="friend-name">${escapeHtml(r.username || r.uuid)}</div></div>
<button class="btn btn-mini" data-act="accept">Annehmen</button>
<button class="btn btn-mini btn-danger" data-act="decline">Ablehnen</button>`;
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
row.querySelector('[data-act="accept"]').addEventListener('click', async () => {
await window.api.friendsRespond(r.uuid, true);
ladeFreunde();
});
row.querySelector('[data-act="decline"]').addEventListener('click', async () => {
await window.api.friendsRespond(r.uuid, false);
ladeFreunde();
});
reqBox.appendChild(row);
}
}
if (gesendetRes.ok && gesendetRes.list.length) {
const titel = document.createElement('div');
titel.className = 'friends-section-title';
titel.textContent = 'Gesendet';
reqBox.appendChild(titel);
for (const r of gesendetRes.list) {
const row = document.createElement('div');
row.className = 'friend-row';
row.innerHTML = `<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(r.uuid)}/64" alt="" />
<div class="friend-info"><div class="friend-name">${escapeHtml(r.username || r.uuid)}</div><div class="friend-status">wartet auf Bestätigung</div></div>
<button class="btn btn-mini btn-danger" data-act="cancel">Zurückziehen</button>`;
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
row.querySelector('[data-act="cancel"]').addEventListener('click', async () => {
await window.api.friendsCancel(r.uuid);
ladeFreunde();
});
reqBox.appendChild(row);
}
}
const listBox = el('friends-list-box');
listBox.innerHTML = '<div class="friends-section-title">Freunde</div>';
if (!freundeRes.ok) {
listBox.innerHTML += `<div class="friends-empty">${escapeHtml(freundeRes.message || 'Nicht erreichbar.')}</div>`;
return;
}
if (!freundeRes.list.length) {
listBox.innerHTML += '<div class="friends-empty">Noch keine Freunde. Namen oben eingeben, um eine Anfrage zu senden.</div>';
return;
}
const sortiert = [...freundeRes.list].sort((a, b) => {
if (a.online !== b.online) return a.online ? -1 : 1;
return (a.username || '').localeCompare(b.username || '');
});
for (const f of sortiert) {
const row = document.createElement('div');
row.className = 'friend-row';
const anzeigeName = spitznamen[f.uuid] || f.username || f.uuid;
const status = f.online
? (f.instance ? `spielt ${f.instance}${f.server ? ' · ' + f.server : ''}` : 'online')
: 'offline';
row.innerHTML = `<span class="friend-dot${f.online ? ' online' : ''}"></span>
<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(f.uuid)}/64" alt="" />
<div class="friend-info"><div class="friend-name">${escapeHtml(anzeigeName)}</div><div class="friend-status">${escapeHtml(status)}</div></div>
<button class="btn btn-mini" data-act="nick" title="Spitzname">✎</button>
<button class="btn btn-mini btn-danger" data-act="remove" title="Entfernen">🗑</button>`;
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
row.querySelector('[data-act="nick"]').addEventListener('click', async () => {
const wert = await askPrompt({
title: 'Spitzname',
label: `Eigener Spitzname für ${f.username || f.uuid} (leer lassen zum Entfernen)`,
value: spitznamen[f.uuid] || '',
okLabel: 'Speichern',
});
if (wert === null) return; // Abgebrochen
await window.api.friendsSetNickname(f.uuid, wert.trim());
ladeFreunde();
});
row.querySelector('[data-act="remove"]').addEventListener('click', () => {
askConfirm('Freund entfernen', `„${anzeigeName}" aus der Freundesliste entfernen?`, '🗑 Entfernen', async () => {
await window.api.friendsRemove(f.uuid);
ladeFreunde();
});
});
listBox.appendChild(row);
}
}
async function sendeFreundschaftsanfrage() {
const input = el('friends-add-input');
const name = input.value.trim();
if (!name) return;
const res = await window.api.friendsAdd(name);
if (res.ok) {
toast('Anfrage gesendet.');
input.value = '';
ladeFreunde();
} else {
toast(res.message || 'Anfrage fehlgeschlagen.', true);
}
}
function renderContentList(boxId, items, sub, emoji) {
const box = el(boxId);
box.innerHTML = '';
@@ -3846,6 +4118,7 @@ async function openSettings(pane) {
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-bedrock').checked = s.bedrockEnabled !== false;
el('s-notify').checked = s.desktopNotifications !== false;
el('s-friends').checked = s.friendsEnabled !== false;
el('s-autostart').checked = !!s.autostart;
el('s-auto-java').checked = s.autoJava !== false;
el('s-autobackup').checked = !!s.autoBackupEnabled;
@@ -3974,6 +4247,7 @@ async function saveSettings() {
iso3dIcons: el('s-iso3d').checked,
bedrockEnabled: el('s-bedrock').checked,
desktopNotifications: el('s-notify').checked,
friendsEnabled: el('s-friends').checked,
autostart: el('s-autostart').checked,
autoJava: el('s-auto-java').checked,
autoBackupEnabled: el('s-autobackup').checked,
@@ -4629,6 +4903,16 @@ function wire() {
document.querySelectorAll('.btab').forEach((t) =>
t.addEventListener('click', () => switchBedrockTab(t.dataset.kind)));
// Freunde
el('btn-friends').addEventListener('click', openFriends);
el('friends-add-btn').addEventListener('click', sendeFreundschaftsanfrage);
el('friends-add-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendeFreundschaftsanfrage();
});
new MutationObserver(() => {
if (el('modal-friends').classList.contains('hidden')) { clearInterval(friendsAutoRefresh); friendsAutoRefresh = null; }
}).observe(el('modal-friends'), { attributes: true, attributeFilter: ['class'] });
// Konsole
el('btn-console').addEventListener('click', openConsole);
el('console-clear').addEventListener('click', () => { consoleBuffer = ''; el('console-out').textContent = ''; });
@@ -4846,7 +5130,14 @@ function wire() {
// Ordner-Buttons in den Inhalte-/Screenshots-Dialogen
el('content-open-worlds').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'saves'));
el('content-open-rp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'resourcepacks'));
el('screens-open').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'screenshots'));
el('screens-open').addEventListener('click', async () => {
if (screensAktiverTab === 'records') {
const inst = await window.api.getInstance(currentDetailId);
if (inst) window.api.recordingOpenFolder(inst.name);
} else {
window.api.openSubdir(currentDetailId, 'screenshots');
}
});
el('screens-select-all').addEventListener('click', () => {
document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => {
if (t.dataset.name) screenSelected.add(t.dataset.name);
@@ -4858,6 +5149,19 @@ function wire() {
updateScreensSelectionUi();
});
el('screens-delete-sel').addEventListener('click', () => deleteSelectedScreenshots());
el('screens-tab-photos').addEventListener('click', () => switchScreensTab('photos'));
el('screens-tab-records').addEventListener('click', () => switchScreensTab('records'));
el('records-select-all').addEventListener('click', () => {
document.querySelectorAll('#records-grid .screen-thumb').forEach((t) => {
if (t.dataset.name) recordSelected.add(t.dataset.name);
});
updateRecordsSelectionUi();
});
el('records-select-none').addEventListener('click', () => {
recordSelected.clear();
updateRecordsSelectionUi();
});
el('records-delete-sel').addEventListener('click', () => deleteSelectedRecords());
el('content-open-dp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'datapacks'));
// Mod-Dialog
@@ -5172,6 +5476,7 @@ const POPOUT_OPENERS = {
}
},
record: () => openRecording(),
friends: () => openFriends(),
};
function popoutBoot(params) {
@@ -5209,7 +5514,22 @@ window.addEventListener('DOMContentLoaded', async () => {
applyTheme(s.theme);
iso3dEnabled = s.iso3dIcons !== false;
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
el('btn-friends').classList.toggle('hidden', s.friendsEnabled === false);
} catch { applyTheme('dark'); }
// Zahl offener Freundschaftsanfragen am "Freunde"-Knopf (nur Hauptfenster
// zeigt die Werkzeugleiste)
if (!POPOUT) {
const zeigeAnfragenZahl = (zahl) => {
const badge = el('friends-badge');
badge.textContent = zahl > 99 ? '99+' : String(zahl);
badge.classList.toggle('hidden', !zahl);
el('btn-friends').title = zahl
? `Freunde – ${zahl} offene Anfrage${zahl === 1 ? '' : 'n'} (ingame: Strg+Alt+F zeigt, wer online ist)`
: 'Freunde (ingame: Strg+Alt+F zeigt, wer online ist)';
};
window.api.onFriendsRequestCount(zeigeAnfragenZahl);
window.api.friendsRequestCount().then(zeigeAnfragenZahl).catch(() => {});
}
await refreshSkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
// Servername live in den Titel nachtragen, falls das Aufzeichnungs-Fenster
@@ -5221,8 +5541,14 @@ window.addEventListener('DOMContentLoaded', async () => {
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await refreshSkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
// Spiel gestartet/beendet ODER das aktive Konto in einem ANDEREN Fenster
// gewechselt (separater Renderer-Prozess, siehe cross-window state) ->
// Kontenlisten, Kopfzeile und Skin-Hintergrund hier ebenfalls nachziehen
window.api.onAccountsChanged(async () => {
await refreshAllAccountLists();
await refreshAccountButton();
await refreshSkinBackground();
});
// über eine Desktop-Verknüpfung gestartet -> Instanz auswählen und anwerfen
// (bei einer Server-Verknüpfung zusätzlich direkt mit dem Server verbinden)
window.api.onLaunchRequest(async (payload) => {
@@ -5247,7 +5573,12 @@ window.addEventListener('DOMContentLoaded', async () => {
window.api.onSettingsChanged((s) => {
applyTheme(s.theme);
iso3dEnabled = s.iso3dIcons !== false;
if (!POPOUT) { applyBedrockVisibility(); refreshSkinBackground(); reload(); }
if (!POPOUT) {
applyBedrockVisibility();
refreshSkinBackground();
reload();
el('btn-friends').classList.toggle('hidden', s.friendsEnabled === false);
}
});
// Ein Skin-Wechsel oder eine neue/aktualisierte Instanz aus einem anderen
// Fenster (Skin-/Import-Popup) betrifft ebenfalls nur die Anzeige im