noah33565 commited on
Commit
0dff6da
Β·
verified Β·
1 Parent(s): 9928a35

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +236 -48
app.js CHANGED
@@ -301,6 +301,7 @@ async function doLogin() {
301
  async function doLogout() {
302
  if (!currentUser) return;
303
  saveSession(null);
 
304
  dbDelete(`online/${currentUser.username}`);
305
  if (voiceActive) leaveVoice();
306
  clearInterval(pollTimer);
@@ -323,6 +324,7 @@ async function launchApp() {
323
  }
324
  updateTopbar();
325
  buildChannelList();
 
326
  openChannel('allgemein');
327
  startPolling();
328
  }
@@ -404,34 +406,38 @@ async function pollTick() {
404
  }
405
  }
406
 
407
- // Abgelaufene Voice-EintrΓ€ge aufrΓ€umen
408
  for (const [vcId, parts] of Object.entries(DB.voice||{})) {
409
  for (const [uname, data] of Object.entries(parts||{})) {
410
- if (now-(data.ts||0) > 12000) dbDelete(`voice/${vcId}/${uname}`);
 
 
411
  }
412
  }
 
 
 
 
 
 
 
 
 
413
  buildChannelList();
414
  }
415
 
416
  // ─── SYNC STATUS INDICATOR ────────────────────────────────────
417
- function setSyncStatus(state) {
418
- let el = document.getElementById('sync-dot');
419
- if (!el) {
420
- el = document.createElement('div');
421
- el.id = 'sync-dot';
422
- el.style.cssText = 'position:fixed;bottom:14px;left:50%;transform:translateX(-50%);z-index:9999;font-family:Orbitron,monospace;font-size:.55rem;letter-spacing:.12em;padding:3px 10px;background:rgba(5,10,15,0.9);border-radius:2px;border:1px solid;transition:all .3s;pointer-events:none';
423
- document.body.appendChild(el);
424
- }
425
- if (state==='sync') {
426
- el.textContent='⟳ SYNC...';
427
- el.style.borderColor='rgba(0,212,255,0.3)';
428
- el.style.color='var(--muted)';
429
- el.style.opacity='1';
430
- } else {
431
- el.textContent='● LIVE';
432
- el.style.borderColor='rgba(78,255,145,0.3)';
433
- el.style.color='var(--green)';
434
- setTimeout(()=>{ if(el) el.style.opacity='0'; }, 1500);
435
  }
436
  }
437
 
@@ -719,42 +725,224 @@ function updateBadge(id,count) {
719
  else el.style.display='none';
720
  }
721
 
722
- // ─── VOICE ────────────────────────────────────────────────────
723
- function joinVoice(vcId,vcName) {
724
- if (voiceActive&&voiceChannel===vcId) { leaveVoice(); return; }
725
- if (voiceActive) leaveVoice();
726
- voiceActive=true; voiceChannel=vcId; micMuted=false; deafened=false;
727
- if (!DB.voice[vcId]) DB.voice[vcId]={};
728
- dbSet(`voice/${vcId}/${currentUser.username}`,{name:currentUser.username,muted:false,ts:Date.now()});
729
- document.getElementById('voice-panel').classList.add('active');
730
- document.getElementById('voice-channel-name').textContent='πŸ”Š '+vcName;
731
- document.getElementById('voice-mute-btn').textContent='πŸŽ™ Stumm';
732
- document.getElementById('voice-deaf-btn').textContent='πŸ”” Taub';
733
- document.getElementById('voice-mute-btn').classList.remove('active');
734
- document.getElementById('voice-deaf-btn').classList.remove('active');
735
- buildChannelList();
736
- showToast(`πŸ”Š Beigetreten: ${vcName}`,'var(--green)');
737
  }
738
- function leaveVoice() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  if (!voiceActive) return;
740
- dbDelete(`voice/${voiceChannel}/${currentUser.username}`);
741
- voiceActive=false; voiceChannel=null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  document.getElementById('voice-panel').classList.remove('active');
743
  buildChannelList();
744
- showToast('Voice verlassen','var(--muted)');
745
  }
 
746
  function toggleMute() {
747
- micMuted=!micMuted;
748
- const btn=document.getElementById('voice-mute-btn');
749
- btn.textContent=micMuted?'πŸ”‡ Stumm':'πŸŽ™ Stumm';
750
- btn.classList.toggle('active',micMuted);
751
- if (voiceActive) dbSet(`voice/${voiceChannel}/${currentUser.username}/muted`,micMuted);
 
 
 
 
 
 
 
 
752
  }
 
753
  function toggleDeafen() {
754
- deafened=!deafened;
755
- const btn=document.getElementById('voice-deaf-btn');
756
- btn.textContent=deafened?'πŸ”• Taub':'πŸ”” Taub';
757
- btn.classList.toggle('active',deafened);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  }
759
 
760
  // ─── USER LIST ────────────────────────────────────────────────
 
301
  async function doLogout() {
302
  if (!currentUser) return;
303
  saveSession(null);
304
+ if (voiceActive) await leaveVoice();
305
  dbDelete(`online/${currentUser.username}`);
306
  if (voiceActive) leaveVoice();
307
  clearInterval(pollTimer);
 
324
  }
325
  updateTopbar();
326
  buildChannelList();
327
+ restoreVoiceState();
328
  openChannel('allgemein');
329
  startPolling();
330
  }
 
406
  }
407
  }
408
 
409
+ // Abgelaufene Voice-EintrΓ€ge aufrΓ€umen (20s Timeout)
410
  for (const [vcId, parts] of Object.entries(DB.voice||{})) {
411
  for (const [uname, data] of Object.entries(parts||{})) {
412
+ if (now-(data.ts||0) > 20000) {
413
+ delete DB.voice[vcId][uname];
414
+ }
415
  }
416
  }
417
+
418
+ // Voice: neue User im Channel erkennen und anrufen
419
+ voicePollConnect();
420
+
421
+ // Voice-Heartbeat: eigenen Eintrag aktuell halten
422
+ if (voiceActive && voiceChannel && DB.voice?.[voiceChannel]?.[currentUser.username]) {
423
+ DB.voice[voiceChannel][currentUser.username].ts = now;
424
+ }
425
+
426
  buildChannelList();
427
  }
428
 
429
  // ─── SYNC STATUS INDICATOR ────────────────────────────────────
430
+ function setSyncStatus(state) { /* deaktiviert – keine stΓΆrende Animation */ }
431
+
432
+ function restoreVoiceState() {
433
+ const vcNames = {'voice-allgemein':'Allgemein','voice-gaming':'Gaming','voice-musik':'Musik'};
434
+ for (const [vcId, parts] of Object.entries(DB.voice||{})) {
435
+ if (parts?.[currentUser.username]) {
436
+ // Nach Reload: Voice neu joinen (stellt PeerJS-Verbindung wieder her)
437
+ const name = vcNames[vcId] || vcId;
438
+ joinVoice(vcId, name);
439
+ break;
440
+ }
 
 
 
 
 
 
 
441
  }
442
  }
443
 
 
725
  else el.style.display='none';
726
  }
727
 
728
+ // ═══════════════════════════════════════════════════════════════
729
+ // REAL VOICE β€” WebRTC via PeerJS
730
+ // ═══════════════════════════════════════════════════════════════
731
+ let myPeer = null; // PeerJS-Instanz
732
+ let localStream = null; // Mikrofon-Stream
733
+ let activeCalls = {}; // { peerId: Call }
734
+ let activeAudios = {}; // { peerId: <audio> }
735
+
736
+ // Peer-ID aus Username + Channel deterministisch ableiten
737
+ // (damit man nach Reload reconnecten kann)
738
+ function makePeerId(username, vcId) {
739
+ // PeerJS IDs dΓΌrfen nur alphanumeric + - _ sein
740
+ const safe = (username + '__' + vcId).replace(/[^a-zA-Z0-9_-]/g, '_');
741
+ return 'nc__' + safe;
 
742
  }
743
+
744
+ async function joinVoice(vcId, vcName) {
745
+ if (voiceActive && voiceChannel === vcId) { leaveVoice(); return; }
746
+ if (voiceActive) await leaveVoice();
747
+
748
+ // Mikrofon anfordern
749
+ try {
750
+ localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
751
+ } catch(e) {
752
+ showToast('πŸŽ™ Mikrofon-Zugriff verweigert!', 'var(--danger)');
753
+ return;
754
+ }
755
+
756
+ voiceActive = true;
757
+ voiceChannel = vcId;
758
+ micMuted = false;
759
+ deafened = false;
760
+
761
+ const peerId = makePeerId(currentUser.username, vcId);
762
+
763
+ // Alten Peer aufrΓ€umen falls vorhanden
764
+ if (myPeer && !myPeer.destroyed) {
765
+ myPeer.destroy();
766
+ myPeer = null;
767
+ }
768
+
769
+ // PeerJS initialisieren
770
+ myPeer = new Peer(peerId, {
771
+ host: 'peerjs-server.netlify.app', // kostenloser public PeerJS server
772
+ secure: true,
773
+ path: '/',
774
+ debug: 0,
775
+ config: {
776
+ iceServers: [
777
+ { urls: 'stun:stun.l.google.com:19302' },
778
+ { urls: 'stun:stun1.l.google.com:19302' },
779
+ ]
780
+ }
781
+ });
782
+
783
+ myPeer.on('open', (id) => {
784
+ console.log('[Voice] Peer offen:', id);
785
+ // In DB eintragen
786
+ if (!DB.voice[vcId]) DB.voice[vcId] = {};
787
+ DB.voice[vcId][currentUser.username] = {
788
+ name: currentUser.username,
789
+ peerId: id,
790
+ muted: false,
791
+ ts: Date.now()
792
+ };
793
+ apiSave();
794
+
795
+ // Mit allen anderen im Channel verbinden
796
+ const others = Object.entries(DB.voice[vcId] || {})
797
+ .filter(([uname]) => uname !== currentUser.username);
798
+ for (const [uname, data] of others) {
799
+ if (data.peerId) callPeer(data.peerId);
800
+ }
801
+
802
+ // Panel anzeigen
803
+ document.getElementById('voice-panel').classList.add('active');
804
+ document.getElementById('voice-channel-name').textContent = 'πŸ”Š ' + vcName;
805
+ document.getElementById('voice-mute-btn').textContent = 'πŸŽ™ Stumm';
806
+ document.getElementById('voice-deaf-btn').textContent = 'πŸ”” Taub';
807
+ document.getElementById('voice-mute-btn').classList.remove('active');
808
+ document.getElementById('voice-deaf-btn').classList.remove('active');
809
+ buildChannelList();
810
+ showToast(`πŸ”Š Beigetreten: ${vcName}`, 'var(--green)');
811
+ });
812
+
813
+ // Eingehende Anrufe entgegennehmen
814
+ myPeer.on('call', (call) => {
815
+ call.answer(localStream);
816
+ handleCallStream(call);
817
+ });
818
+
819
+ myPeer.on('error', (err) => {
820
+ // ID bereits vergeben β†’ mit Suffix nochmal versuchen
821
+ if (err.type === 'unavailable-id') {
822
+ console.warn('[Voice] Peer-ID besetzt, versuche disconnect...');
823
+ // Alten Eintrag lΓΆschen und nochmal joinen
824
+ if (DB.voice?.[vcId]?.[currentUser.username]) {
825
+ delete DB.voice[vcId][currentUser.username];
826
+ apiSave();
827
+ }
828
+ setTimeout(() => joinVoice(vcId, vcName), 1500);
829
+ } else {
830
+ console.warn('[Voice] Peer-Fehler:', err.type, err.message);
831
+ showToast('πŸ”Š Voice-Fehler: ' + err.type, 'var(--warn)');
832
+ }
833
+ });
834
+
835
+ myPeer.on('disconnected', () => {
836
+ if (voiceActive) myPeer.reconnect();
837
+ });
838
+ }
839
+
840
+ function callPeer(remotePeerId) {
841
+ if (!localStream || !myPeer || activeCalls[remotePeerId]) return;
842
+ console.log('[Voice] Rufe an:', remotePeerId);
843
+ const call = myPeer.call(remotePeerId, localStream);
844
+ if (!call) return;
845
+ handleCallStream(call);
846
+ }
847
+
848
+ function handleCallStream(call) {
849
+ activeCalls[call.peer] = call;
850
+ call.on('stream', (remoteStream) => {
851
+ if (activeAudios[call.peer]) {
852
+ activeAudios[call.peer].srcObject = remoteStream;
853
+ return;
854
+ }
855
+ const audio = document.createElement('audio');
856
+ audio.autoplay = true;
857
+ audio.srcObject = remoteStream;
858
+ document.body.appendChild(audio);
859
+ activeAudios[call.peer] = audio;
860
+ console.log('[Voice] Stream von:', call.peer);
861
+ });
862
+ call.on('close', () => {
863
+ cleanupPeer(call.peer);
864
+ });
865
+ call.on('error', (e) => {
866
+ console.warn('[Voice] Call-Fehler:', e);
867
+ cleanupPeer(call.peer);
868
+ });
869
+ }
870
+
871
+ function cleanupPeer(peerId) {
872
+ if (activeCalls[peerId]) { try { activeCalls[peerId].close(); } catch {} delete activeCalls[peerId]; }
873
+ if (activeAudios[peerId]) { activeAudios[peerId].remove(); delete activeAudios[peerId]; }
874
+ }
875
+
876
+ async function leaveVoice() {
877
  if (!voiceActive) return;
878
+ const ch = voiceChannel;
879
+ voiceActive = false;
880
+ voiceChannel = null;
881
+
882
+ // Alle Calls beenden
883
+ for (const pid of Object.keys(activeCalls)) cleanupPeer(pid);
884
+ activeCalls = {};
885
+
886
+ // Mikrofon stoppen
887
+ if (localStream) {
888
+ localStream.getTracks().forEach(t => t.stop());
889
+ localStream = null;
890
+ }
891
+
892
+ // Peer zerstΓΆren
893
+ if (myPeer && !myPeer.destroyed) {
894
+ myPeer.destroy();
895
+ myPeer = null;
896
+ }
897
+
898
+ // DB-Eintrag entfernen
899
+ if (DB.voice?.[ch]?.[currentUser.username]) {
900
+ delete DB.voice[ch][currentUser.username];
901
+ apiSave();
902
+ }
903
+
904
  document.getElementById('voice-panel').classList.remove('active');
905
  buildChannelList();
906
+ showToast('Voice verlassen', 'var(--muted)');
907
  }
908
+
909
  function toggleMute() {
910
+ micMuted = !micMuted;
911
+ const btn = document.getElementById('voice-mute-btn');
912
+ btn.textContent = micMuted ? 'πŸ”‡ Stumm' : 'πŸŽ™ Stumm';
913
+ btn.classList.toggle('active', micMuted);
914
+ // Mikrofon-Track stumm/laut schalten
915
+ if (localStream) {
916
+ localStream.getAudioTracks().forEach(t => { t.enabled = !micMuted; });
917
+ }
918
+ if (voiceActive && DB.voice?.[voiceChannel]?.[currentUser.username]) {
919
+ DB.voice[voiceChannel][currentUser.username].muted = micMuted;
920
+ DB.voice[voiceChannel][currentUser.username].ts = Date.now();
921
+ apiSave();
922
+ }
923
  }
924
+
925
  function toggleDeafen() {
926
+ deafened = !deafened;
927
+ const btn = document.getElementById('voice-deaf-btn');
928
+ btn.textContent = deafened ? 'πŸ”• Taub' : 'πŸ”” Taub';
929
+ btn.classList.toggle('active', deafened);
930
+ // Alle eingehenden Audios stumm/laut schalten
931
+ for (const audio of Object.values(activeAudios)) {
932
+ audio.muted = deafened;
933
+ }
934
+ }
935
+
936
+ // Im Poll-Tick: neue User im Channel erkennen und anrufen
937
+ function voicePollConnect() {
938
+ if (!voiceActive || !voiceChannel || !myPeer || myPeer.disconnected) return;
939
+ const others = Object.entries(DB.voice[voiceChannel] || {})
940
+ .filter(([uname]) => uname !== currentUser.username);
941
+ for (const [uname, data] of others) {
942
+ if (data.peerId && !activeCalls[data.peerId]) {
943
+ callPeer(data.peerId);
944
+ }
945
+ }
946
  }
947
 
948
  // ─── USER LIST ────────────────────────────────────────────────