Files

661 lines
22 KiB
JavaScript
Raw Permalink Normal View History

2026-03-25 14:14:07 +01:00
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);
});