Update app.js
Browse files
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) >
|
|
|
|
|
|
|
| 411 |
}
|
| 412 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
buildChannelList();
|
| 414 |
}
|
| 415 |
|
| 416 |
// βββ SYNC STATUS INDICATOR ββββββββββββββββββββββββββββββββββββ
|
| 417 |
-
function setSyncStatus(state) {
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 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 |
-
//
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
showToast(`π Beigetreten: ${vcName}`,'var(--green)');
|
| 737 |
}
|
| 738 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
if (!voiceActive) return;
|
| 740 |
-
|
| 741 |
-
voiceActive=false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββ
|