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 = '
Gespeicherte Verbindungen
'; 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 = '' + id + '' + '' + timeStr + '' + ''; 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 = ' 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 = ' 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 = ' 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 = ' 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); });