Files
aza/APP/nexus-remote/renderer.js
2026-03-25 14:14:07 +01:00

661 lines
22 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 { ipcRenderer } = require('electron');
const Peer = require('peerjs').default || require('peerjs');
let input = null;
try { input = require('./input'); } catch {}
// ─── DOM ───
const myIdEl = document.getElementById('my-id');
const remoteIdInput = document.getElementById('remote-id');
const remoteKeyInput = document.getElementById('remote-key');
const btnConnect = document.getElementById('btn-connect');
const btnShare = document.getElementById('btn-share');
const btnCopy = document.getElementById('btn-copy');
const btnKill = document.getElementById('btn-kill');
const btnRenew = document.getElementById('btn-renew');
const myKeyEl = document.getElementById('my-key');
const killOverlay = document.getElementById('kill-overlay');
const killClose = document.getElementById('kill-close');
const rcIndicator = document.getElementById('rc-indicator');
const cbAot = document.getElementById('cb-aot');
const cbCtrl = document.getElementById('cb-ctrl');
const remoteVideo = document.getElementById('remote-video');
const statusEl = document.getElementById('status-text');
const statusDot = document.getElementById('dot');
const placeholder = document.getElementById('placeholder');
const toast = document.getElementById('toast');
const trustBadge = document.getElementById('trust-badge');
const savedPeersEl = document.getElementById('saved-peers');
// ─── Persistente Zugangsdaten ───
function generateId() { return String(Math.floor(100000 + Math.random() * 900000)); }
function generateKey() {
const c = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let k = '';
for (let i = 0; i < 8; i++) k += c[Math.floor(Math.random() * c.length)];
return k;
}
let MY_ID = localStorage.getItem('savedPeerId');
let SECRET_KEY = localStorage.getItem('savedSecretKey');
if (!MY_ID || !SECRET_KEY) {
MY_ID = generateId();
SECRET_KEY = generateKey();
localStorage.setItem('savedPeerId', MY_ID);
localStorage.setItem('savedSecretKey', SECRET_KEY);
}
myKeyEl.textContent = SECRET_KEY;
// ─── Gespeicherte Verbindungen ───
let savedConns = JSON.parse(localStorage.getItem('savedConnections') || '{}');
function saveSavedConns() {
localStorage.setItem('savedConnections', JSON.stringify(savedConns));
renderSavedPeers();
}
// ─── State ───
let localStream = null;
let sharing = false;
let dataConn = null;
let incomingConn = null;
let activeCall = null;
let incomingCall = null;
let remoteW = null;
let remoteH = null;
let windowFocused = true;
let killed = false;
let connected = false;
let authenticatedPeers = new Set();
window.addEventListener('focus', () => { windowFocused = true; });
window.addEventListener('blur', () => { windowFocused = false; });
// ─── Auto-fill letzte Verbindung ───
const lastPeerId = localStorage.getItem('lastConnectedPeer') || '';
if (lastPeerId) {
remoteIdInput.value = lastPeerId;
if (savedConns[lastPeerId]) {
remoteKeyInput.value = savedConns[lastPeerId].key;
}
}
updateTrustBadge();
function updateTrustBadge() {
const tid = remoteIdInput.value.trim();
if (savedConns[tid]) {
trustBadge.classList.add('show');
if (!remoteKeyInput.value.trim()) {
remoteKeyInput.value = savedConns[tid].key;
}
remoteKeyInput.classList.add('trusted');
} else {
trustBadge.classList.remove('show');
remoteKeyInput.classList.remove('trusted');
}
}
remoteIdInput.addEventListener('input', () => {
const tid = remoteIdInput.value.trim();
if (savedConns[tid]) {
remoteKeyInput.value = savedConns[tid].key;
} else {
remoteKeyInput.value = '';
}
updateTrustBadge();
});
// ─── Gespeicherte Peers im Placeholder anzeigen ───
function renderSavedPeers() {
if (!savedPeersEl) return;
const peers = Object.entries(savedConns);
if (peers.length === 0) {
savedPeersEl.classList.remove('show');
return;
}
savedPeersEl.innerHTML = '<div class="saved-peers-title">Gespeicherte Verbindungen</div>';
peers.sort((a, b) => (b[1].lastUsed || 0) - (a[1].lastUsed || 0));
peers.forEach(([id, data]) => {
const item = document.createElement('div');
item.className = 'saved-peer-item';
const timeStr = data.lastUsed ? new Date(data.lastUsed).toLocaleDateString('de-DE') : '';
item.innerHTML =
'<span class="saved-peer-id">' + id + '</span>' +
'<span class="saved-peer-time">' + timeStr + '</span>' +
'<button class="saved-peer-remove" title="Entfernen">&times;</button>';
item.querySelector('.saved-peer-id').addEventListener('click', () => {
remoteIdInput.value = id;
remoteKeyInput.value = data.key;
updateTrustBadge();
});
item.querySelector('.saved-peer-remove').addEventListener('click', (e) => {
e.stopPropagation();
delete savedConns[id];
saveSavedConns();
updateTrustBadge();
});
savedPeersEl.appendChild(item);
});
savedPeersEl.classList.add('show');
}
renderSavedPeers();
// ─── UI Helpers ───
function setStatus(msg, color) {
statusEl.textContent = msg;
if (color) statusEl.style.color = color;
}
function setOnline(on) {
statusDot.classList.toggle('online', on);
}
function showToast(msg, isKill) {
toast.textContent = msg;
toast.className = 'toast show' + (isKill ? ' kill' : '');
setTimeout(() => toast.classList.remove('show'), 2200);
}
function updateRcIndicator() {
const active = connected && ((dataConn && dataConn.open) || (incomingConn && incomingConn.open));
rcIndicator.classList.toggle('show', !!active && cbCtrl.checked && !killed);
}
// ═══════════════════════════════════════════
// DISCONNECT: Verbindung trennen, Peer bleibt
// ═══════════════════════════════════════════
function disconnect() {
if (activeCall) { try { activeCall.close(); } catch {} activeCall = null; }
if (incomingCall) { try { incomingCall.close(); } catch {} incomingCall = null; }
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
if (incomingConn) { try { incomingConn.close(); } catch {} incomingConn = null; }
remoteVideo.srcObject = null;
connected = false;
remoteW = null;
remoteH = null;
rcIndicator.classList.remove('show');
placeholder.classList.remove('hidden');
document.body.classList.add('no-stream');
if (!killed) {
setStatus('Bereit warte auf Verbindung', '#2ea043');
}
showToast('Verbindung getrennt');
}
// ═══════════════════════════════════════════
// KILL: Alles zerstören (Notaus)
// ═══════════════════════════════════════════
function killAll(showOverlay) {
disconnect();
if (localStream) {
localStream.getTracks().forEach((t) => t.stop());
localStream = null;
}
sharing = false;
try { peer.disconnect(); } catch {}
try { peer.destroy(); } catch {}
killed = true;
setOnline(false);
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Screen teilen';
btnShare.classList.remove('active');
setStatus('OFFLINE Alle Verbindungen gekappt', '#e5534b');
if (showOverlay) {
killOverlay.classList.add('show');
}
}
function hardKill() {
killAll(false);
const h2 = killOverlay.querySelector('h2');
const p = killOverlay.querySelector('p');
if (h2) h2.textContent = 'SYSTEM OFFLINE';
if (p) p.textContent = 'ALLE VERBINDUNGEN GEKAPPT';
killOverlay.classList.add('show');
setTimeout(() => ipcRenderer.send('cleanup-done'), 1000);
}
btnKill.addEventListener('click', () => disconnect());
killClose.addEventListener('click', () => killOverlay.classList.remove('show'));
ipcRenderer.on('kill-switch', () => disconnect());
ipcRenderer.on('hard-kill', () => hardKill());
function guardKilled() {
if (killed) {
setStatus('Session beendet App neustarten', '#e5534b');
return true;
}
return false;
}
// ─── PeerJS ───
const peer = new Peer(MY_ID, {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun3.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:19302' },
],
},
});
peer.on('open', (id) => {
if (killed) return;
myIdEl.textContent = id;
setOnline(true);
setStatus('Bereit warte auf Verbindung', '#2ea043');
ipcRenderer.invoke('save-credentials', MY_ID, SECRET_KEY);
autoShare();
});
async function autoShare() {
if (sharing || killed) return;
try {
const sources = await ipcRenderer.invoke('get-sources');
if (!sources.length) return;
localStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sources[0].id,
maxWidth: 1920, maxHeight: 1080, maxFrameRate: 30,
}},
});
sharing = true;
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Stop';
btnShare.classList.add('active');
setStatus('Bereit warte auf Verbindung', '#2ea043');
} catch {}
}
peer.on('error', (err) => {
if (killed) return;
if (err.type === 'unavailable-id') {
setStatus('ID belegt neuer Versuch...', '#e5534b');
setTimeout(() => { if (!killed) location.reload(); }, 800);
return;
}
setOnline(false);
setStatus('Fehler: ' + err.type, '#e5534b');
});
peer.on('disconnected', () => {
if (killed) return;
setOnline(false);
setStatus('Server-Verbindung verloren Reconnect...', '#e5534b');
setTimeout(() => {
if (!killed && !peer.destroyed) {
try { peer.reconnect(); } catch {}
}
}, 3000);
});
// ─── ID + Key kopieren ───
btnCopy.addEventListener('click', () => {
const id = myIdEl.textContent;
if (id && id !== '------') {
navigator.clipboard.writeText(id + ' / ' + SECRET_KEY);
btnCopy.classList.add('copied');
showToast('ID + Key kopiert!');
setTimeout(() => btnCopy.classList.remove('copied'), 2000);
}
});
cbAot.addEventListener('change', () => ipcRenderer.send('set-always-on-top', cbAot.checked));
cbCtrl.addEventListener('change', () => updateRcIndicator());
btnRenew.addEventListener('click', () => {
if (guardKilled()) return;
MY_ID = generateId();
SECRET_KEY = generateKey();
localStorage.setItem('savedPeerId', MY_ID);
localStorage.setItem('savedSecretKey', SECRET_KEY);
showToast('Neue Zugangsdaten generiert Neustart...');
ipcRenderer.invoke('save-credentials', MY_ID, SECRET_KEY).then(() => {
setTimeout(() => location.reload(), 1200);
});
});
// ═══════════════════════════════════════════
// EINGEHENDE VERBINDUNG: Auth prüfen
// ═══════════════════════════════════════════
peer.on('connection', (conn) => {
if (killed) { conn.close(); return; }
conn.on('open', () => {
if (killed) { conn.close(); return; }
});
conn.on('data', (d) => {
if (d.t === 'auth') {
if (d.key === SECRET_KEY) {
authenticatedPeers.add(conn.peer);
conn.send({ t: 'auth-ok' });
showToast('Verbindung akzeptiert: ' + conn.peer);
incomingConn = conn;
connected = true;
if (input) {
const s = input.getScreenSize();
conn.send({ t: 'scr', w: s.width, h: s.height });
}
updateRcIndicator();
setStatus('Verbunden mit ' + conn.peer, '#00b4d8');
} else {
conn.send({ t: 'auth-fail' });
showToast('Zugang verweigert: falscher Key', true);
setTimeout(() => conn.close(), 500);
}
return;
}
handleRemoteData(d);
});
conn.on('close', () => {
if (incomingConn === conn) {
incomingConn = null;
connected = false;
updateRcIndicator();
placeholder.classList.remove('hidden');
document.body.classList.add('no-stream');
remoteVideo.srcObject = null;
if (!killed) {
setStatus('Bereit warte auf Verbindung', '#2ea043');
showToast('Gegenstelle hat getrennt');
}
}
});
});
// ─── Eingehender Anruf: nur authentifizierte Peers ───
peer.on('call', (call) => {
if (killed) return;
if (!authenticatedPeers.has(call.peer)) {
showToast('Anruf abgelehnt: nicht authentifiziert', true);
return;
}
call.answer(localStream || undefined);
incomingCall = call;
call.on('stream', (stream) => {
if (killed) return;
remoteVideo.srcObject = stream;
placeholder.classList.add('hidden');
document.body.classList.remove('no-stream');
setStatus('Verbunden mit ' + call.peer, '#00b4d8');
});
call.on('close', () => {
if (incomingCall === call) { incomingCall = null; }
});
});
// ─── Remote Data Handler ───
function handleRemoteData(d) {
if (killed) return;
if (d.t === 'scr') {
remoteW = d.w; remoteH = d.h;
return;
}
if (!input) return;
if (!cbCtrl.checked) return;
try {
switch (d.t) {
case 'mm': input.moveMouse(d.x|0, d.y|0); break;
case 'md': input.moveMouse(d.x|0, d.y|0); input.mouseToggle('down', d.b||'left'); break;
case 'mu': input.mouseToggle('up', d.b||'left'); break;
case 'mc': input.moveMouse(d.x|0, d.y|0); input.mouseClick(d.b||'left'); break;
case 'sc': input.scrollMouse(d.dx||0, d.dy||0); break;
case 'kd': input.keyToggle(d.k, 'down'); break;
case 'ku': input.keyToggle(d.k, 'up'); break;
}
} catch {}
}
// ─── Screen Share Toggle ───
btnShare.addEventListener('click', async () => {
if (guardKilled()) return;
if (sharing) {
if (localStream) localStream.getTracks().forEach((t) => t.stop());
localStream = null;
sharing = false;
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Screen teilen';
btnShare.classList.remove('active');
setStatus('Stream gestoppt', '#8b949e');
return;
}
try {
const sources = await ipcRenderer.invoke('get-sources');
if (!sources.length) { setStatus('Keine Quelle!', '#e5534b'); return; }
localStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sources[0].id,
maxWidth: 1920, maxHeight: 1080, maxFrameRate: 30,
}},
});
sharing = true;
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Stop';
btnShare.classList.add('active');
setStatus('Capture aktiv', '#2ea043');
} catch (e) {
setStatus('Fehler: ' + e.message, '#e5534b');
}
});
// ═══════════════════════════════════════════
// VERBINDEN: Auth-Handshake + Screen-Call
// ═══════════════════════════════════════════
btnConnect.addEventListener('click', async () => {
if (guardKilled()) return;
const tid = remoteIdInput.value.trim();
let tkey = remoteKeyInput.value.trim();
if (!tid) { setStatus('Remote-ID eingeben!', '#e5534b'); return; }
if (!tkey && savedConns[tid]) {
tkey = savedConns[tid].key;
remoteKeyInput.value = tkey;
}
if (!tkey) {
setStatus('Remote-Key eingeben!', '#e5534b');
remoteKeyInput.focus();
return;
}
if (!sharing || !localStream) await autoShare();
if (!localStream) { setStatus('Screen-Capture fehlgeschlagen!', '#e5534b'); return; }
setStatus('Verbinde...', '#e5534b');
btnConnect.disabled = true;
dataConn = peer.connect(tid, { reliable: true });
const authTimeout = setTimeout(() => {
if (!connected) {
setStatus('Zeitüberschreitung Gegenstelle nicht erreichbar', '#e5534b');
btnConnect.disabled = false;
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
}
}, 15000);
dataConn.on('open', () => {
if (killed) return;
setStatus('Authentifiziere...', '#e5534b');
dataConn.send({ t: 'auth', key: tkey });
});
dataConn.on('data', (d) => {
if (d.t === 'auth-ok') {
clearTimeout(authTimeout);
connected = true;
btnConnect.disabled = false;
savedConns[tid] = { key: tkey, lastUsed: Date.now() };
saveSavedConns();
localStorage.setItem('lastConnectedPeer', tid);
updateTrustBadge();
activeCall = peer.call(tid, localStream);
activeCall.on('stream', (s) => {
if (killed) return;
remoteVideo.srcObject = s;
placeholder.classList.add('hidden');
document.body.classList.remove('no-stream');
});
if (input) {
const s = input.getScreenSize();
dataConn.send({ t: 'scr', w: s.width, h: s.height });
}
setStatus('Verbunden: ' + tid, '#00b4d8');
showToast('Verbunden mit ' + tid);
updateRcIndicator();
return;
}
if (d.t === 'auth-fail') {
clearTimeout(authTimeout);
btnConnect.disabled = false;
setStatus('Falscher Key! Zugang verweigert.', '#e5534b');
showToast('Falscher Key!', true);
if (savedConns[tid]) {
delete savedConns[tid];
saveSavedConns();
updateTrustBadge();
remoteKeyInput.value = '';
}
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
return;
}
handleRemoteData(d);
});
dataConn.on('close', () => {
clearTimeout(authTimeout);
if (connected) {
connected = false;
dataConn = null;
updateRcIndicator();
showToast('Verbindung getrennt');
if (!killed) setStatus('Bereit warte auf Verbindung', '#2ea043');
placeholder.classList.remove('hidden');
document.body.classList.add('no-stream');
remoteVideo.srcObject = null;
} else {
btnConnect.disabled = false;
dataConn = null;
}
});
dataConn.on('error', (err) => {
clearTimeout(authTimeout);
btnConnect.disabled = false;
setStatus('Verbindungsfehler: ' + (err.type || err.message || err), '#e5534b');
});
});
// ─── Koordinaten skalieren ───
function scale(e) {
const r = remoteVideo.getBoundingClientRect();
return {
x: ((e.clientX - r.left) / r.width) * (remoteW || remoteVideo.videoWidth || 1920),
y: ((e.clientY - r.top) / r.height) * (remoteH || remoteVideo.videoHeight || 1080),
};
}
function sendCtrl(obj) {
if (killed || !connected) return;
if (!cbCtrl.checked) return;
const conn = dataConn || incomingConn;
if (!conn || !conn.open) return;
conn.send(obj);
}
// ─── Maus-Events ───
remoteVideo.addEventListener('mousemove', (e) => {
const c = scale(e); sendCtrl({ t:'mm', x:c.x, y:c.y });
});
remoteVideo.addEventListener('mousedown', (e) => {
e.preventDefault();
const c = scale(e);
sendCtrl({ t:'md', x:c.x, y:c.y, b:['left','middle','right'][e.button]||'left' });
});
remoteVideo.addEventListener('mouseup', (e) => {
e.preventDefault();
sendCtrl({ t:'mu', b:['left','middle','right'][e.button]||'left' });
});
remoteVideo.addEventListener('wheel', (e) => {
if (!cbCtrl.checked || killed || !connected) return;
e.preventDefault();
sendCtrl({ t:'sc', dx:Math.sign(e.deltaX)*3, dy:Math.sign(e.deltaY)*3 });
}, { passive: false });
remoteVideo.addEventListener('contextmenu', (e) => e.preventDefault());
// ─── Tastatur ───
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'Escape') { disconnect(); return; }
if (killed || !cbCtrl.checked || !connected) return;
e.preventDefault();
sendCtrl({ t:'kd', k:mapKey(e.key) });
});
document.addEventListener('keyup', (e) => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'Escape') return;
if (killed || !cbCtrl.checked || !connected) return;
e.preventDefault();
sendCtrl({ t:'ku', k:mapKey(e.key) });
});
function mapKey(k) {
const m = {
Enter:'enter',Backspace:'backspace',Delete:'delete',Tab:'tab',
' ':'space',
ArrowUp:'up',ArrowDown:'down',ArrowLeft:'left',ArrowRight:'right',
Home:'home',End:'end',PageUp:'pageup',PageDown:'pagedown',
Control:'control',Shift:'shift',Alt:'alt',Insert:'insert',
F1:'f1',F2:'f2',F3:'f3',F4:'f4',F5:'f5',F6:'f6',
F7:'f7',F8:'f8',F9:'f9',F10:'f10',F11:'f11',F12:'f12',
};
return m[k] || k.toLowerCase();
}
// ─── Auto-Dim ───
let dimTimer;
document.addEventListener('mousemove', () => {
const bar = document.getElementById('bar');
bar.classList.remove('dim');
clearTimeout(dimTimer);
if (!document.body.classList.contains('no-stream')) {
dimTimer = setTimeout(() => bar.classList.add('dim'), 3000);
}
});
// ─── Fenster-Close Guard ───
window.addEventListener('beforeunload', () => {
if (!killed) killAll(false);
});