// EuerIRC Client Frontend Application (function () { 'use strict'; // --- MIRC IRC Color Palette (Optimized for Dark Mode readability) --- const mircColors = [ '#ffffff', // 0: White '#555a64', // 1: Black (visible dark-grey for dark background) '#3b82f6', // 2: Blue '#10b981', // 3: Green '#ef4444', // 4: Red '#b91c1c', // 5: Brown / Dark Red '#a855f7', // 6: Purple '#f97316', // 7: Orange '#facc15', // 8: Yellow '#4ade80', // 9: Light Green '#14b8a6', // 10: Teal / Cyan '#38bdf8', // 11: Light Cyan '#60a5fa', // 12: Light Blue '#f472b6', // 13: Pink / Magenta '#9ca3af', // 14: Grey '#e5e7eb' // 15: Light Grey ]; // Font mappings const fontMap = { system: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', monospace: 'Consolas, "Courier New", Courier, monospace', segoe: '"Segoe UI", -apple-system, sans-serif', inter: '"Inter", -apple-system, sans-serif', arial: 'Arial, sans-serif', verdana: 'Verdana, sans-serif', trebuchet: '"Trebuchet MS", sans-serif' }; // Density mappings const densityMap = { compact: { lineHeight: '1.25', gap: '3px' }, normal: { lineHeight: '1.5', gap: '6px' }, cozy: { lineHeight: '1.8', gap: '10px' } }; // Direktverbindung: feste Daten des Radio-Chats, nur Name und optionales Passwort sind frei. const RADIO_IRC = { host: 'irc.euer.tv', port: 6697, channel: '#erde', ssl: true }; // Default settings const defaultSettings = { fontFamily: 'system', customFont: '', fontSize: 14, lineHeight: 'normal', timestampFormat: 'hm', ircColors: true, emotesEnabled: true, emoteSize: 26, highlightNick: true, autoScroll: true, showMediaArea: true, activeConnection: 'direct', // Direct Connection Settings directHost: 'irc.euer.tv', directPort: 6697, directNick: '', directChannel: '#erde', directPass: '', directSsl: true, directAutoConnect: false, // ZNC Bouncer Connection Settings zncHost: '', zncPort: 8250, zncUser: '', zncPass: '', zncNick: '', zncChannel: '#erde', zncSsl: false, zncAutoConnect: false, // Nur fuer selbstsignierte Zertifikate (unsicher); wird manuell in der config.json gesetzt acceptInvalidCerts: false, // Oberflaechen-Skalierung in Prozent uiScale: 100, // Manuell sortierte Reihenfolge der Kanal-Tabs channelOrder: [], autoUpdateCheck: true, // Lua & Modi settings luaEnabled: false, luaFileName: '', normalNick: '', modiNick: '', internPass: '', nickservPasswords: {} }; let currentSettings = { ...defaultSettings }; // Aeltere Nachrichten werden verworfen, damit Speicher und DOM bei Dauerbetrieb nicht wachsen. const MAX_MESSAGES_PER_CHANNEL = 1000; // --- State --- const state = { connected: false, connecting: false, currentChannel: '#erde', currentNick: '', channels: { '#erde': { topic: 'Verbinde mit irc.euer.tv:6697...', messages: [], users: [], unread: 0 }, '*server*': { topic: 'IRC Server Systemmeldungen & Konsole', messages: [], users: [], unread: 0 } }, emotes: { baseUrl: 'https://chat.euer.tv/static/emocache/', map: {}, sortedKeys: [] }, isModiActive: false }; function getTauri() { return window.__TAURI__ || null; } // --- DOM Elements --- const el = { statusDot: document.getElementById('statusDot'), statusText: document.getElementById('statusText'), statusPing: document.getElementById('statusPing'), btnHeaderConnect: document.getElementById('btnHeaderConnect'), headerChanName: document.getElementById('headerChanName'), headerTopic: document.getElementById('headerTopic'), btnModiToggle: document.getElementById('btnModiToggle'), modiToggleLabel: document.getElementById('modiToggleLabel'), modiDot: document.getElementById('modiDot'), channelList: document.getElementById('channelList'), messagesContainer: document.getElementById('messagesContainer'), chatInput: document.getElementById('chatInput'), btnSend: document.getElementById('btnSend'), userCount: document.getElementById('userCount'), userList: document.getElementById('userList'), displayNick: document.getElementById('displayNick'), displayHost: document.getElementById('displayHost'), userAvatar: document.getElementById('userAvatar'), // Emote picker btnEmotePicker: document.getElementById('btnEmotePicker'), emotePicker: document.getElementById('emotePicker'), btnCloseEmote: document.getElementById('btnCloseEmote'), emoteSearch: document.getElementById('emoteSearch'), emoteGrid: document.getElementById('emoteGrid'), emoteTabs: document.querySelectorAll('.emote-tab'), // Unified Settings Modal btnSettingsModal: document.getElementById('btnSettingsModal'), settingsModal: document.getElementById('settingsModal'), btnCloseSettingsModal: document.getElementById('btnCloseSettingsModal'), btnSaveSettings: document.getElementById('btnSaveSettings'), btnResetSettings: document.getElementById('btnResetSettings'), settingsTabs: document.querySelectorAll('.modal-tabs .tab-btn'), tabBtnRadio: document.getElementById('tabBtnRadio'), // Display pane optFontFamily: document.getElementById('optFontFamily'), optFontCustom: document.getElementById('optFontCustom'), optFontSize: document.getElementById('optFontSize'), valFontSize: document.getElementById('valFontSize'), optLineHeight: document.getElementById('optLineHeight'), optTimestamp: document.getElementById('optTimestamp'), optUiScale: document.getElementById('optUiScale'), // Updates updCurrentVersion: document.getElementById('updCurrentVersion'), optAutoUpdate: document.getElementById('optAutoUpdate'), btnCheckUpdate: document.getElementById('btnCheckUpdate'), updStatusText: document.getElementById('updStatusText'), updateModal: document.getElementById('updateModal'), updSummary: document.getElementById('updSummary'), updNotes: document.getElementById('updNotes'), updProgress: document.getElementById('updProgress'), updProgressBar: document.getElementById('updProgressBar'), updModalStatus: document.getElementById('updModalStatus'), btnUpdateInstall: document.getElementById('btnUpdateInstall'), btnUpdateLater: document.getElementById('btnUpdateLater'), btnCloseUpdate: document.getElementById('btnCloseUpdate'), optIrcColors: document.getElementById('optIrcColors'), optEmotes: document.getElementById('optEmotes'), optEmoteSize: document.getElementById('optEmoteSize'), optHighlight: document.getElementById('optHighlight'), optAutoScroll: document.getElementById('optAutoScroll'), optShowHeaderTrack: document.getElementById('optShowHeaderTrack'), optShowMediaArea: document.getElementById('optShowMediaArea'), settingsPreview: document.getElementById('settingsPreview'), // Server & ZNC pane btnTypeDirect: document.getElementById('btnTypeDirect'), btnTypeZnc: document.getElementById('btnTypeZnc'), formDirect: document.getElementById('formDirect'), formZnc: document.getElementById('formZnc'), directHost: document.getElementById('directHost'), directPort: document.getElementById('directPort'), directNick: document.getElementById('directNick'), directChannel: document.getElementById('directChannel'), directPass: document.getElementById('directPass'), directSsl: document.getElementById('directSsl'), directAutoConnect: document.getElementById('directAutoConnect'), btnSaveDirect: document.getElementById('btnSaveDirect'), btnConnectNow: document.getElementById('btnConnectNow'), btnClearDirect: document.getElementById('btnClearDirect'), directSaveFeedback: document.getElementById('directSaveFeedback'), zncHost: document.getElementById('zncHost'), zncPort: document.getElementById('zncPort'), zncUser: document.getElementById('zncUser'), zncPass: document.getElementById('zncPass'), zncNick: document.getElementById('zncNick'), zncChannel: document.getElementById('zncChannel'), zncSsl: document.getElementById('zncSsl'), zncAutoConnect: document.getElementById('zncAutoConnect'), btnSaveZnc: document.getElementById('btnSaveZnc'), btnConnectZncNow: document.getElementById('btnConnectZncNow'), btnClearZnc: document.getElementById('btnClearZnc'), zncSaveFeedback: document.getElementById('zncSaveFeedback'), // Radio & Modi pane cfgNormalNick: document.getElementById('cfgNormalNick'), cfgModiNick: document.getElementById('cfgModiNick'), cfgInternPass: document.getElementById('cfgInternPass'), tabBtnRadio: document.getElementById('tabBtnRadio'), secRadio: document.getElementById('secRadio'), // Lua pane tabBtnLua: document.getElementById('tabBtnLua'), secLua: document.getElementById('secLua'), luaPickerBox: document.getElementById('luaPickerBox'), luaModiDetails: document.getElementById('luaModiDetails'), btnSelectLua: document.getElementById('btnSelectLua'), luaFileInput: document.getElementById('luaFileInput'), luaFileName: document.getElementById('luaFileName'), chkEnableLua: document.getElementById('chkEnableLua'), scriptStatusText: document.getElementById('scriptStatusText'), loadedScriptsList: document.getElementById('loadedScriptsList'), btnAddChan: document.getElementById('btnAddChan'), // Header & Track Area headerLeft: document.querySelector('.header-left'), headerArtist: document.getElementById('headerArtist'), headerTrack: document.getElementById('headerTrack'), headerCoverBox: document.getElementById('headerCoverBox'), streamCoverImg: document.getElementById('streamCoverImg'), // Sidebar Media Area (Cover/Track, Video, Listeners, Grußbox) sidebarMediaArea: document.getElementById('sidebarMediaArea'), sidebarTrackHeader: document.getElementById('sidebarTrackHeader'), sidebarCoverBox: document.getElementById('sidebarCoverBox'), sidebarCoverImg: document.getElementById('sidebarCoverImg'), sidebarTrackInfo: document.getElementById('sidebarTrackInfo'), sidebarArtist: document.getElementById('sidebarArtist'), sidebarTrack: document.getElementById('sidebarTrack'), sidebarBottomBar: document.getElementById('sidebarBottomBar'), statTotalListeners: document.getElementById('statTotalListeners'), statVideoListeners: document.getElementById('statVideoListeners'), statAudioListeners: document.getElementById('statAudioListeners'), btnOpenGrussbox: document.getElementById('btnOpenGrussbox'), grussboxModal: document.getElementById('grussboxModal'), btnCloseGrussbox: document.getElementById('btnCloseGrussbox'), btnCancelGruss: document.getElementById('btnCancelGruss'), btnSubmitGruss: document.getElementById('btnSubmitGruss'), gbNameInput: document.getElementById('gbNameInput'), gbTextInput: document.getElementById('gbTextInput'), gbFeedback: document.getElementById('gbFeedback'), // Resizer & Sidebar sidebarResizer: document.getElementById('sidebarResizer'), sidebarUsers: document.querySelector('.sidebar-users'), videoQualitySelect: document.getElementById('videoQualitySelect'), // Sidebar Video Player Controls btnStreamMute: document.getElementById('btnStreamMute'), streamMuteIcon: document.getElementById('streamMuteIcon'), streamVolSlider: document.getElementById('streamVolSlider'), btnResyncStream: document.getElementById('btnResyncStream'), streamVideo: document.getElementById('streamVideo'), // User Context Menu Popover userContextMenu: document.getElementById('userContextMenu'), ctxUserPrefix: document.getElementById('ctxUserPrefix'), ctxUserNick: document.getElementById('ctxUserNick') }; // --- Settings Management --- async function loadSettings() { // Fruehere Versionen haben Einstellungen (inkl. Passwoertern) im localStorage dupliziert. try { localStorage.removeItem('euerirc_settings'); localStorage.removeItem('euerirc_last_nick'); } catch (e) { /* ignorieren */ } const tauri = getTauri(); if (tauri && tauri.core) { try { const fileContent = await tauri.core.invoke('load_app_config'); if (fileContent) { currentSettings = { ...defaultSettings, ...JSON.parse(fileContent) }; } } catch (err) { // config.json existiert beim ersten Start noch nicht und wird beim Speichern angelegt } } applySettings(currentSettings); } function setConnectionTab(type) { currentSettings.activeConnection = type; if (type === 'znc') { if (el.btnTypeZnc) el.btnTypeZnc.classList.add('active'); if (el.btnTypeDirect) el.btnTypeDirect.classList.remove('active'); if (el.formZnc) { el.formZnc.classList.remove('hidden'); el.formZnc.classList.add('active'); } if (el.formDirect) { el.formDirect.classList.add('hidden'); el.formDirect.classList.remove('active'); } } else { if (el.btnTypeDirect) el.btnTypeDirect.classList.add('active'); if (el.btnTypeZnc) el.btnTypeZnc.classList.remove('active'); if (el.formDirect) { el.formDirect.classList.remove('hidden'); el.formDirect.classList.add('active'); } if (el.formZnc) { el.formZnc.classList.add('hidden'); el.formZnc.classList.remove('active'); } } } let appliedUiScale = 100; function applyUiScale(percent) { const scale = Number(percent) || 100; if (scale === appliedUiScale) return; const tauri = getTauri(); if (!tauri || !tauri.core) return; tauri.core.invoke('set_ui_zoom', { factor: scale / 100 }) .then(() => { appliedUiScale = scale; }) .catch((err) => console.warn('Skalierung fehlgeschlagen:', err)); } function applySettings(s) { const root = document.documentElement; let fontCss = fontMap.system; if (s.customFont && s.customFont.trim().length > 0) { fontCss = `"${s.customFont.trim()}", sans-serif`; } else if (s.fontFamily && s.fontFamily !== 'system') { fontCss = fontMap[s.fontFamily] || `"${s.fontFamily}", sans-serif`; } const density = densityMap[s.lineHeight] || densityMap.normal; root.style.setProperty('--chat-font-family', fontCss); root.style.setProperty('--chat-font-size', `${s.fontSize}px`); root.style.setProperty('--chat-line-height', density.lineHeight); root.style.setProperty('--chat-msg-gap', density.gap); root.style.setProperty('--chat-emote-size', `${s.emoteSize}px`); applyUiScale(s.uiScale); if (el.optUiScale) el.optUiScale.value = String(s.uiScale || 100); if (el.optAutoUpdate) el.optAutoUpdate.checked = s.autoUpdateCheck !== false; // Visibility toggles if (el.sidebarMediaArea) { const isVisible = s.showMediaArea !== false; el.sidebarMediaArea.classList.toggle('hidden', !isVisible); if (!isVisible) { stopStreamPlayer(); } else { if (!hlsInstance && (!el.streamVideo || el.streamVideo.paused || !el.streamVideo.src)) { startStreamPlayer(); } } } // Populate modal controls if (el.optFontFamily) el.optFontFamily.value = s.fontFamily || 'system'; if (el.optFontCustom) el.optFontCustom.value = s.customFont || ''; if (el.optFontSize) el.optFontSize.value = s.fontSize; if (el.valFontSize) el.valFontSize.textContent = `${s.fontSize}px`; if (el.optLineHeight) el.optLineHeight.value = s.lineHeight; if (el.optTimestamp) el.optTimestamp.value = s.timestampFormat; if (el.optIrcColors) el.optIrcColors.checked = s.ircColors; if (el.optEmotes) el.optEmotes.checked = s.emotesEnabled; if (el.optEmoteSize) el.optEmoteSize.value = s.emoteSize; if (el.optHighlight) el.optHighlight.checked = s.highlightNick; if (el.optAutoScroll) el.optAutoScroll.checked = s.autoScroll; if (el.optShowMediaArea) el.optShowMediaArea.checked = s.showMediaArea !== false; // Direct Connection fields const directNick = s.directNick || s.normalNick || ''; if (el.directNick) el.directNick.value = directNick; if (el.directPass) el.directPass.value = s.directPass || ''; if (el.directAutoConnect) el.directAutoConnect.checked = !!s.directAutoConnect; // ZNC Connection fields if (el.zncHost) el.zncHost.value = s.zncHost || ''; if (el.zncPort) el.zncPort.value = s.zncPort || 8250; if (el.zncUser) el.zncUser.value = s.zncUser || ''; if (el.zncPass) el.zncPass.value = s.zncPass || ''; if (el.zncNick) el.zncNick.value = s.zncNick || ''; if (el.zncChannel) el.zncChannel.value = s.zncChannel || '#erde'; if (el.zncSsl) el.zncSsl.checked = !!s.zncSsl; if (el.zncAutoConnect) el.zncAutoConnect.checked = !!s.zncAutoConnect; // Connection tab activation setConnectionTab(s.activeConnection || 'direct'); // Radio & Modi fields const activeNick = (s.activeConnection === 'znc' && s.zncNick) ? s.zncNick : directNick; if (el.cfgNormalNick) el.cfgNormalNick.value = s.normalNick || activeNick || ''; if (el.cfgModiNick) el.cfgModiNick.value = s.modiNick || ''; if (el.cfgInternPass) el.cfgInternPass.value = s.internPass || ''; // Lua fields if (el.chkEnableLua) el.chkEnableLua.checked = !!s.luaEnabled; if (el.luaFileName && s.luaFileName) el.luaFileName.textContent = s.luaFileName; updateLuaUI(); updatePreview(); } function updateLuaUI() { const isLoaded = !!(currentSettings.luaEnabled && currentSettings.luaFileName); if (el.luaModiDetails) { el.luaModiDetails.classList.toggle('hidden', !isLoaded); } if (isLoaded) { if (el.btnModiToggle) el.btnModiToggle.classList.remove('hidden'); if (el.tabBtnRadio) el.tabBtnRadio.classList.remove('hidden'); if (el.chkEnableLua) el.chkEnableLua.checked = true; if (el.luaFileName) el.luaFileName.textContent = currentSettings.luaFileName; if (el.scriptStatusText) { el.scriptStatusText.textContent = `✓ Aktiv: ${currentSettings.luaFileName}`; el.scriptStatusText.style.color = 'var(--accent-green)'; } } else { if (el.btnModiToggle) el.btnModiToggle.classList.add('hidden'); if (el.tabBtnRadio) el.tabBtnRadio.classList.add('hidden'); if (el.chkEnableLua) el.chkEnableLua.checked = false; if (el.luaFileName) el.luaFileName.textContent = 'Keine Datei ausgewählt'; if (el.scriptStatusText) { el.scriptStatusText.textContent = '○ Inaktiv (Kein Skript geladen)'; el.scriptStatusText.style.color = 'var(--text-muted)'; } const activeTab = document.querySelector('.modal-tabs .tab-btn.active'); if (activeTab && activeTab.dataset.tabTarget === 'secRadio') { const displayTab = document.querySelector('.modal-tabs .tab-btn[data-tab-target="secDisplay"]'); if (displayTab) displayTab.click(); } } } async function persistConfigToFileAndStorage() { const tauri = getTauri(); if (tauri && tauri.core) { try { await tauri.core.invoke('save_app_config', { configContent: JSON.stringify(currentSettings, null, 2) }); } catch (err) { console.warn('Dateispeicherung fehlgeschlagen:', err); } } } async function saveDirectSettings(showNotification = true) { const nick = el.directNick ? el.directNick.value.trim() : (currentSettings.directNick || ''); const pass = el.directPass ? el.directPass.value.trim() : ''; const autoConn = el.directAutoConnect ? el.directAutoConnect.checked : false; currentSettings.activeConnection = 'direct'; currentSettings.directHost = RADIO_IRC.host; currentSettings.directPort = RADIO_IRC.port; currentSettings.directNick = nick; currentSettings.directChannel = RADIO_IRC.channel; currentSettings.directPass = pass; currentSettings.directSsl = RADIO_IRC.ssl; currentSettings.directAutoConnect = autoConn; if (nick) currentSettings.normalNick = nick; state.currentNick = nick; if (el.displayNick) el.displayNick.textContent = nick || 'Nicht verbunden'; if (el.userAvatar) el.userAvatar.textContent = nick ? nick[0].toUpperCase() : '?'; await persistConfigToFileAndStorage(); if (showNotification && el.directSaveFeedback) { el.directSaveFeedback.textContent = '✅ Direkt-Einstellungen gespeichert!'; el.directSaveFeedback.classList.remove('hidden'); setTimeout(() => { if (el.directSaveFeedback) el.directSaveFeedback.classList.add('hidden'); }, 3500); } } async function saveZncSettings(showNotification = true) { const host = el.zncHost ? el.zncHost.value.trim() : ''; const port = parseInt(el.zncPort ? el.zncPort.value : '8250', 10) || 8250; const user = el.zncUser ? el.zncUser.value.trim() : ''; const pass = el.zncPass ? el.zncPass.value.trim() : ''; const nick = (el.zncNick && el.zncNick.value.trim()) || (user ? user.split('/')[0] : 'Hoerer'); const channel = (el.zncChannel && el.zncChannel.value.trim()) || '#erde'; const ssl = el.zncSsl ? el.zncSsl.checked : false; const autoConn = el.zncAutoConnect ? el.zncAutoConnect.checked : false; currentSettings.activeConnection = 'znc'; currentSettings.zncHost = host; currentSettings.zncPort = port; currentSettings.zncUser = user; currentSettings.zncPass = pass; currentSettings.zncNick = nick; currentSettings.zncChannel = channel; currentSettings.zncSsl = ssl; currentSettings.zncAutoConnect = autoConn; currentSettings.normalNick = nick; state.currentNick = nick; if (el.displayNick) el.displayNick.textContent = nick; if (el.userAvatar) el.userAvatar.textContent = nick ? nick[0].toUpperCase() : '?'; await persistConfigToFileAndStorage(); if (showNotification && el.zncSaveFeedback) { el.zncSaveFeedback.textContent = '✅ ZNC-Einstellungen gespeichert!'; el.zncSaveFeedback.classList.remove('hidden'); setTimeout(() => { if (el.zncSaveFeedback) el.zncSaveFeedback.classList.add('hidden'); }, 3500); } } function clearDirectForm() { if (el.directNick) el.directNick.value = ''; if (el.directPass) el.directPass.value = ''; if (el.directAutoConnect) el.directAutoConnect.checked = false; } function clearZncForm() { if (el.zncHost) el.zncHost.value = ''; if (el.zncPort) el.zncPort.value = '8250'; if (el.zncUser) el.zncUser.value = ''; if (el.zncPass) el.zncPass.value = ''; if (el.zncNick) el.zncNick.value = ''; if (el.zncChannel) el.zncChannel.value = '#erde'; if (el.zncSsl) el.zncSsl.checked = false; if (el.zncAutoConnect) el.zncAutoConnect.checked = false; } async function saveSettings() { const isZncActive = currentSettings.activeConnection === 'znc'; const chosenNick = isZncActive ? ((el.zncNick && el.zncNick.value.trim()) || (el.zncUser && el.zncUser.value.trim().split('/')[0]) || '') : ((el.directNick && el.directNick.value.trim()) || (el.cfgNormalNick && el.cfgNormalNick.value.trim()) || ''); currentSettings = { ...currentSettings, fontFamily: el.optFontFamily ? el.optFontFamily.value : 'system', customFont: el.optFontCustom ? el.optFontCustom.value.trim() : '', fontSize: parseInt(el.optFontSize.value, 10) || 14, lineHeight: el.optLineHeight.value, timestampFormat: el.optTimestamp.value, uiScale: el.optUiScale ? parseInt(el.optUiScale.value, 10) || 100 : 100, autoUpdateCheck: el.optAutoUpdate ? el.optAutoUpdate.checked : true, ircColors: el.optIrcColors.checked, emotesEnabled: el.optEmotes.checked, emoteSize: parseInt(el.optEmoteSize.value, 10) || 26, highlightNick: el.optHighlight.checked, autoScroll: el.optAutoScroll.checked, showMediaArea: el.optShowMediaArea ? el.optShowMediaArea.checked : true, // Direct directHost: RADIO_IRC.host, directPort: RADIO_IRC.port, directNick: el.directNick ? el.directNick.value.trim() : currentSettings.directNick, directChannel: RADIO_IRC.channel, directPass: el.directPass ? el.directPass.value.trim() : '', directSsl: RADIO_IRC.ssl, directAutoConnect: el.directAutoConnect ? el.directAutoConnect.checked : false, // ZNC zncHost: el.zncHost ? el.zncHost.value.trim() : currentSettings.zncHost, zncPort: el.zncPort ? parseInt(el.zncPort.value, 10) || 8250 : 8250, zncUser: el.zncUser ? el.zncUser.value.trim() : currentSettings.zncUser, zncPass: el.zncPass ? el.zncPass.value.trim() : currentSettings.zncPass, zncNick: el.zncNick ? el.zncNick.value.trim() : currentSettings.zncNick, zncChannel: el.zncChannel ? el.zncChannel.value.trim() || '#erde' : '#erde', zncSsl: el.zncSsl ? el.zncSsl.checked : false, zncAutoConnect: el.zncAutoConnect ? el.zncAutoConnect.checked : false, // Lua & Modi luaEnabled: el.chkEnableLua ? (el.chkEnableLua.checked && !!currentSettings.luaFileName) : false, luaFileName: currentSettings.luaFileName || '', normalNick: chosenNick, modiNick: el.cfgModiNick ? el.cfgModiNick.value.trim() : '', internPass: el.cfgInternPass ? el.cfgInternPass.value.trim() : '', nickservPasswords: currentSettings.nickservPasswords || {} }; state.currentNick = chosenNick; if (el.displayNick) el.displayNick.textContent = chosenNick || 'Nicht verbunden'; if (el.userAvatar) el.userAvatar.textContent = chosenNick ? chosenNick[0].toUpperCase() : '?'; await persistConfigToFileAndStorage(); applySettings(currentSettings); renderCurrentChannelMessages(); el.settingsModal.classList.add('hidden'); } function updatePreview() { if (!el.settingsPreview) return; let fFamily = fontMap.system; if (el.optFontCustom && el.optFontCustom.value.trim().length > 0) { fFamily = `"${el.optFontCustom.value.trim()}", sans-serif`; } else if (el.optFontFamily && el.optFontFamily.value !== 'system') { fFamily = fontMap[el.optFontFamily.value] || `"${el.optFontFamily.value}", sans-serif`; } const fSize = `${el.optFontSize.value}px`; const density = densityMap[el.optLineHeight.value] || densityMap.normal; const eSize = `${el.optEmoteSize.value}px`; el.settingsPreview.style.fontFamily = fFamily; el.settingsPreview.style.fontSize = fSize; el.settingsPreview.style.lineHeight = density.lineHeight; el.settingsPreview.style.gap = density.gap; const emotes = el.settingsPreview.querySelectorAll('.chat-emote'); emotes.forEach(img => { img.style.height = eSize; }); } // --- Modi Nickwechsel (Radio Sendebetrieb) --- function toggleModiState() { const tauri = getTauri(); if (!state.connected && tauri) { addMessage('*server*', { type: 'system', text: '❌ Bitte zuerst mit dem Server verbinden.' }); return; } if (!state.isModiActive) { // Switch to Modi const normalName = state.currentNick || currentSettings.normalNick || 'Hoerer'; currentSettings.normalNick = normalName; if (el.cfgNormalNick) el.cfgNormalNick.value = normalName; let modiName = currentSettings.modiNick; if (!modiName) { modiName = prompt('Bitte deinen Moderatoren-Nick eingeben (z. B. DJ_Name):', `DJ_${normalName}`); if (!modiName) return; currentSettings.modiNick = sanitizeIrcNick(modiName); if (el.cfgModiNick) el.cfgModiNick.value = currentSettings.modiNick; } modiName = sanitizeIrcNick(modiName); const internPass = currentSettings.internPass || ''; state.isModiActive = true; el.btnModiToggle.classList.add('modi-active'); el.modiToggleLabel.textContent = `🎧 Modi aktiv (${modiName})`; addMessage('*server*', { type: 'system', text: `📻 [Sendebetrieb AN] Wechsle zu Moderatoren-Nick '${modiName}' und betrete #intern...` }); // NickServ-Identify beim Nickwechsel uebernimmt das Backend. if (tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `NICK ${modiName}` }); tauri.core.invoke('send_irc_raw', { line: internPass ? `JOIN #intern ${internPass}` : 'JOIN #intern' }); } addChannelToState('#intern'); switchChannel('#intern'); } else { // Switch back to Normal const normalName = currentSettings.normalNick || state.currentNick.replace(/^DJ_/i, '') || 'Hoerer'; state.isModiActive = false; el.btnModiToggle.classList.remove('modi-active'); el.modiToggleLabel.textContent = '📻 Zum Modi wechseln'; addMessage('*server*', { type: 'system', text: `📻 [Sendebetrieb AUS] Verlasse #intern und wechsle zurück zu '${normalName}'...` }); if (tauri && tauri.core) { // Erst !deop mit dem Modi-Nick in allen offenen Kanaelen (ausser #intern) senden, dann nach // kurzer Pause PART und Nickwechsel, damit der Bot den Befehl noch dem Modi-Nick zuordnet. const deopChans = Object.keys(state.channels) .filter(name => name.startsWith('#') && name.toLowerCase() !== '#intern'); for (const chan of deopChans) { tauri.core.invoke('send_irc_raw', { line: `PRIVMSG ${chan} :!deop` }); } el.btnModiToggle.disabled = true; setTimeout(() => { tauri.core.invoke('send_irc_raw', { line: 'PART #intern :Sendung beendet' }); tauri.core.invoke('send_irc_raw', { line: `NICK ${normalName}` }); el.btnModiToggle.disabled = false; }, 1000); } if (state.channels['#intern']) { delete state.channels['#intern']; } switchChannel('#erde'); updateChannelListUI(); } } // --- Initialize Emotes --- async function loadEmotes() { try { const res = await fetch('emotes.json'); const data = await res.json(); state.emotes.baseUrl = data.base_url || 'https://chat.euer.tv/static/emocache/'; state.emotes.map = data.emotes || {}; state.emotes.sortedKeys = Object.keys(state.emotes.map).sort((a, b) => b.length - a.length); emoteRegex = null; renderEmoteGrid('all'); } catch (err) { console.warn('Emotes konnten nicht geladen werden:', err); } } function renderEmoteGrid(filterType, search = '') { el.emoteGrid.innerHTML = ''; const q = search.toLowerCase(); for (const code of state.emotes.sortedKeys) { const filename = state.emotes.map[code]; const isGif = filename.toLowerCase().endsWith('.gif'); if (filterType === 'gifs' && !isGif) continue; if (filterType === 'smilies' && isGif) continue; if (q && !code.toLowerCase().includes(q) && !filename.toLowerCase().includes(q)) continue; const item = document.createElement('div'); item.className = 'emote-item'; item.title = code; const img = document.createElement('img'); img.src = state.emotes.baseUrl + filename; img.alt = code; img.loading = 'lazy'; item.appendChild(img); item.addEventListener('click', () => { insertEmoteToInput(code); }); el.emoteGrid.appendChild(item); } } function insertEmoteToInput(code) { const val = el.chatInput.value; const space = val.length > 0 && !val.endsWith(' ') ? ' ' : ''; el.chatInput.value = val + space + code + ' '; el.chatInput.focus(); // Do NOT close emotePicker here so user can click multiple emotes/GIFs consecutively! } // --- Comprehensive mIRC Formatting & Emote Parser --- // Escapes auch Anfuehrungszeichen, damit Text sicher in HTML-Attribute passt. function escapeHtml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatIrcText(rawText) { if (!rawText) return ''; let out = ''; const enableColors = currentSettings.ircColors; if (!enableColors) { // Strip control codes completely if disabled let stripped = rawText.replace(/\x03(?:\d{1,2}(?:,\d{1,2})?)?/g, '') .replace(/[\x02\x1D\x1F\x1E\x0F\x16]/g, ''); out = escapeHtml(stripped); } else { // Parse full mIRC formatting let inColor = false; let inBold = false; let inItalic = false; let inUnderline = false; let inStrike = false; let i = 0; function closeColor() { if (inColor) { out += ''; inColor = false; } } function closeAll() { closeColor(); if (inBold) { out += ''; inBold = false; } if (inItalic) { out += ''; inItalic = false; } if (inUnderline) { out += ''; inUnderline = false; } if (inStrike) { out += ''; inStrike = false; } } while (i < rawText.length) { const ch = rawText[i]; const code = rawText.charCodeAt(i); if (code === 3) { // Color \x03 i++; const match = rawText.slice(i).match(/^(\d{1,2})(?:,(\d{1,2}))?/); closeColor(); if (match) { const fg = parseInt(match[1], 10) % 16; let bgStyle = ''; if (match[2] !== undefined) { const bg = parseInt(match[2], 10) % 16; bgStyle = `;background-color:${mircColors[bg]};padding:0 2px;border-radius:2px`; } out += ``; inColor = true; i += match[0].length; } continue; } else if (code === 2) { // Bold \x02 inBold = !inBold; out += inBold ? '' : ''; i++; continue; } else if (code === 29) { // Italic \x1D inItalic = !inItalic; out += inItalic ? '' : ''; i++; continue; } else if (code === 31) { // Underline \x1F inUnderline = !inUnderline; out += inUnderline ? '' : ''; i++; continue; } else if (code === 30) { // Strikethrough \x1E inStrike = !inStrike; out += inStrike ? '' : ''; i++; continue; } else if (code === 15) { // Reset \x0F closeAll(); i++; continue; } else if (code === 22) { // Reverse video \x16 i++; continue; } else { // Escape HTML if (ch === '<') out += '<'; else if (ch === '>') out += '>'; else if (ch === '&') out += '&'; else if (ch === '"') out += '"'; else if (ch === "'") out += '''; else out += ch; i++; } } closeAll(); } // Linkify URLs (Text ist bereits escaped, Anfuehrungszeichen kommen als " an) out = out.replace(/(https?:\/\/[^\s<]+)/g, '$1'); // Replace emotes if enabled (ein einziger vorkompilierter Regex statt eines Regex pro Emote) if (currentSettings.emotesEnabled) { const emoteRegex = getEmoteRegex(); if (emoteRegex) { out = out.replace(emoteRegex, (match, lead, code) => { const filename = emoteByEscapedCode.get(code); if (!filename) return match; return `${lead}${code}`; }); } } return out; } let emoteRegex = null; const emoteByEscapedCode = new Map(); function getEmoteRegex() { if (emoteRegex !== null) return emoteRegex || null; emoteByEscapedCode.clear(); if (state.emotes.sortedKeys.length === 0) return null; const alternatives = []; for (const code of state.emotes.sortedKeys) { const escaped = escapeHtml(code); emoteByEscapedCode.set(escaped, state.emotes.map[code]); alternatives.push(escapeRegex(escaped)); } emoteRegex = new RegExp(`(^|\\s)(${alternatives.join('|')})(?=\\s|$)`, 'g'); return emoteRegex; } function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function getNickColor(nick) { const colors = [ '#ff6b6b', '#48dbfb', '#1dd1a1', '#feca57', '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43', '#10ac84' ]; let hash = 0; for (let i = 0; i < nick.length; i++) { hash = nick.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function formatTimestamp(timeStr) { if (currentSettings.timestampFormat === 'none') return ''; const now = new Date(); if (currentSettings.timestampFormat === 'hms') { return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } return timeStr || now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function addMessage(targetChan, msg) { if (!targetChan) targetChan = '*server*'; // Prevent creating tabs named after own nick or CTCP noise if (state.currentNick && targetChan.toLowerCase() === state.currentNick.toLowerCase()) { targetChan = '*server*'; } if (targetChan.includes('\x01') || targetChan.toUpperCase().includes('VERSION')) { targetChan = '*server*'; } if (!state.channels[targetChan]) { addChannelToState(targetChan); } const chanData = state.channels[targetChan]; chanData.messages.push(msg); const overflow = chanData.messages.length - MAX_MESSAGES_PER_CHANNEL; if (overflow > 0) { chanData.messages.splice(0, overflow); } if (state.currentChannel === targetChan) { renderSingleMessage(msg); while (el.messagesContainer.childElementCount > MAX_MESSAGES_PER_CHANNEL) { el.messagesContainer.firstElementChild.remove(); } if (currentSettings.autoScroll) { scrollToBottom(); } } else { chanData.unread++; updateChannelListUI(); } } function renderSingleMessage(msg) { const item = document.createElement('div'); item.className = 'chat-msg'; // Nick highlight check if (currentSettings.highlightNick && state.currentNick && msg.text) { if (msg.text.toLowerCase().includes(state.currentNick.toLowerCase())) { item.classList.add('highlight-msg'); } } if (msg.type === 'system') { item.classList.add('system-msg'); } else if (msg.type === 'notice') { item.classList.add('notice-msg'); } else if (msg.type === 'highlight') { item.classList.add('highlight-msg'); } else if (msg.type === 'action') { item.classList.add('action-msg'); } // Timestamp if (currentSettings.timestampFormat !== 'none') { const timeSpan = document.createElement('span'); timeSpan.className = 'msg-time'; timeSpan.textContent = formatTimestamp(msg.time); item.appendChild(timeSpan); } // Render CTCP Action cleanly (e.g. "* Kellnerin Jetzt läuft: ...") if (msg.type === 'action') { const textSpan = document.createElement('span'); textSpan.className = 'msg-text'; const nickColor = getNickColor(msg.nick || ''); textSpan.innerHTML = `* ${escapeHtml(msg.nick || '')}${formatIrcText(msg.text)}`; item.appendChild(textSpan); el.messagesContainer.appendChild(item); return; } // Normal message: Nick column if (msg.nick) { const nickSpan = document.createElement('span'); nickSpan.className = 'msg-nick'; nickSpan.textContent = msg.nick; nickSpan.style.color = getNickColor(msg.nick); item.appendChild(nickSpan); } // Message Text const textSpan = document.createElement('span'); textSpan.className = 'msg-text'; textSpan.innerHTML = formatIrcText(msg.text); item.appendChild(textSpan); el.messagesContainer.appendChild(item); } function renderCurrentChannelMessages() { el.messagesContainer.innerHTML = ''; const chan = state.channels[state.currentChannel]; if (!chan) return; for (const msg of chan.messages) { renderSingleMessage(msg); } if (currentSettings.autoScroll) { scrollToBottom(); } } function scrollToBottom() { el.messagesContainer.scrollTop = el.messagesContainer.scrollHeight; } // --- Channel Management (Horizontal Bottom Bar) --- function addChannelToState(chanName) { if (state.channels[chanName]) return; state.channels[chanName] = { topic: `Kanal ${chanName}`, messages: [], users: [], unread: 0 }; updateChannelListUI(); } function closeChannel(name) { if (name === '*server*') return; const tauri = getTauri(); if (name.startsWith('#') && state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `PART ${name} :Fenster geschlossen` }); } delete state.channels[name]; if (state.currentChannel === name) { const remaining = Object.keys(state.channels); const nextChan = state.channels['#erde'] ? '#erde' : (remaining[0] || '*server*'); switchChannel(nextChan); } else { updateChannelListUI(); } } function switchChannel(chanName) { if (!state.channels[chanName]) { addChannelToState(chanName); } state.currentChannel = chanName; state.channels[chanName].unread = 0; if (el.headerChanName) el.headerChanName.textContent = chanName; if (el.headerTopic) el.headerTopic.textContent = state.channels[chanName].topic || ''; if (el.chatInput) el.chatInput.placeholder = `Nachricht an ${chanName}... (Enter zum Senden)`; updateChannelListUI(); renderCurrentChannelMessages(); renderUserList(); } // Reihenfolge der Tabs: manuell gespeicherte zuerst, neue Kanaele hinten in Beitrittsreihenfolge. function orderedChannelNames() { const order = currentSettings.channelOrder || []; const rank = (name) => { const i = order.indexOf(name); return i === -1 ? order.length : i; }; return Object.keys(state.channels) .map((name, index) => ({ name, index })) .sort((a, b) => rank(a.name) - rank(b.name) || a.index - b.index) .map(entry => entry.name); } let tabDrag = null; let suppressTabClick = false; function onTabMouseDown(e, item) { if (e.button !== 0 || e.target.closest('.btn-tab-close')) return; tabDrag = { item, startX: e.clientX, active: false }; window.addEventListener('mousemove', onTabDragMove); window.addEventListener('mouseup', onTabDragEnd); } function onTabDragMove(e) { if (!tabDrag) return; if (!tabDrag.item.isConnected) { onTabDragEnd(); return; } if (!tabDrag.active) { if (Math.abs(e.clientX - tabDrag.startX) < 6) return; tabDrag.active = true; tabDrag.item.classList.add('dragging'); document.body.classList.add('tab-dragging'); } // Bei vielen Tabs am Rand der Leiste automatisch mitscrollen. const bar = el.channelList.parentElement.getBoundingClientRect(); if (e.clientX < bar.left + 30) el.channelList.parentElement.scrollLeft -= 14; else if (e.clientX > bar.right - 30) el.channelList.parentElement.scrollLeft += 14; const others = [...el.channelList.children].filter(c => c !== tabDrag.item); const before = others.find((c) => { const r = c.getBoundingClientRect(); return e.clientX < r.left + r.width / 2; }); if (before) { if (tabDrag.item.nextElementSibling !== before) { el.channelList.insertBefore(tabDrag.item, before); } } else if (tabDrag.item.nextElementSibling) { el.channelList.appendChild(tabDrag.item); } } function onTabDragEnd() { window.removeEventListener('mousemove', onTabDragMove); window.removeEventListener('mouseup', onTabDragEnd); const drag = tabDrag; tabDrag = null; if (!drag || !drag.active) return; drag.item.classList.remove('dragging'); document.body.classList.remove('tab-dragging'); // Das auf das Loslassen folgende click-Event soll keinen Kanalwechsel ausloesen. suppressTabClick = true; setTimeout(() => { suppressTabClick = false; }, 0); currentSettings.channelOrder = [...el.channelList.children].map(c => c.dataset.target); persistConfigToFileAndStorage(); updateChannelListUI(); } function updateChannelListUI() { // Waehrend des Ziehens nicht neu aufbauen; passiert nach dem Loslassen. if (tabDrag && tabDrag.active) return; el.channelList.innerHTML = ''; for (const name of orderedChannelNames()) { const item = document.createElement('div'); const isActive = state.currentChannel === name; const isPm = !name.startsWith('#') && name !== '*server*'; const unread = state.channels[name].unread || 0; item.className = 'channel-tab' + (isActive ? ' active' : '') + (isPm ? ' is-pm' : '') + (unread > 0 && !isActive ? ' has-unread' : ''); item.dataset.target = name; const prefix = document.createElement('span'); prefix.className = 'chan-prefix'; prefix.textContent = name.startsWith('#') ? '#' : (name === '*server*' ? 'ℹ' : '💬'); item.appendChild(prefix); const title = document.createElement('span'); title.className = 'chan-name'; title.textContent = name.startsWith('#') ? name.slice(1) : (name === '*server*' ? 'Server' : name); item.appendChild(title); if (unread > 0 && !isActive) { const badge = document.createElement('span'); badge.className = 'chan-badge unread-badge'; badge.textContent = unread; item.appendChild(badge); } // Close button (allow closing any tab except *server*) if (name !== '*server*') { const btnClose = document.createElement('button'); btnClose.className = 'btn-tab-close'; btnClose.innerHTML = '×'; btnClose.title = `${name} schließen`; btnClose.addEventListener('click', (e) => { e.stopPropagation(); closeChannel(name); }); item.appendChild(btnClose); } item.addEventListener('mousedown', (e) => onTabMouseDown(e, item)); item.addEventListener('click', () => { if (!suppressTabClick) switchChannel(name); }); el.channelList.appendChild(item); } } // --- User List Management --- function setUserList(chanName, users) { if (!state.channels[chanName]) { addChannelToState(chanName); } state.channels[chanName].users = users; if (state.currentChannel === chanName) { renderUserList(); } } function renderUserList() { el.userList.innerHTML = ''; const chan = state.channels[state.currentChannel]; if (!chan) { el.userCount.textContent = '0'; return; } if (!state.currentChannel.startsWith('#') && state.currentChannel !== '*server*' && (!chan.users || chan.users.length === 0)) { chan.users = [{ nick: state.currentChannel, prefix: '' }]; } if (!chan.users) { el.userCount.textContent = '0'; return; } el.userCount.textContent = chan.users.length; // Sort: Ops (@, ~, &) first, Voice (+) second, regular last const sorted = [...chan.users].sort((a, b) => { const isOpA = ['@', '~', '&'].includes(a.prefix); const isOpB = ['@', '~', '&'].includes(b.prefix); const rankA = isOpA ? 3 : (a.prefix === '+' ? 2 : 1); const rankB = isOpB ? 3 : (b.prefix === '+' ? 2 : 1); if (rankA !== rankB) return rankB - rankA; return a.nick.localeCompare(b.nick); }); for (const u of sorted) { const item = document.createElement('div'); item.className = 'user-item'; item.title = `Klick: Optionen für ${u.nick} | Doppelklick: PN öffnen`; const prefix = document.createElement('span'); const isOp = ['@', '~', '&'].includes(u.prefix); prefix.className = 'user-prefix ' + (isOp ? 'op' : (u.prefix === '+' ? 'voice' : '')); prefix.textContent = u.prefix || ''; item.appendChild(prefix); const name = document.createElement('span'); name.className = 'user-name'; name.textContent = u.nick; item.appendChild(name); // Single click: Show options menu item.addEventListener('click', (e) => { openUserContextMenu(e, u); }); // Right click also opens options menu item.addEventListener('contextmenu', (e) => { openUserContextMenu(e, u); }); // Double click: Open private chat item.addEventListener('dblclick', () => { closeUserContextMenu(); addChannelToState(u.nick); switchChannel(u.nick); }); el.userList.appendChild(item); } } let activeContextUser = null; function openUserContextMenu(e, user) { e.preventDefault(); e.stopPropagation(); activeContextUser = user; if (!el.userContextMenu) return; if (el.ctxUserPrefix) { el.ctxUserPrefix.textContent = user.prefix || ''; const isOp = ['@', '~', '&'].includes(user.prefix); el.ctxUserPrefix.style.color = isOp ? 'var(--accent-red)' : (user.prefix === '+' ? 'var(--accent-green)' : 'var(--accent-cyan)'); } if (el.ctxUserNick) { el.ctxUserNick.textContent = user.nick; } el.userContextMenu.classList.remove('hidden'); const menuWidth = 205; const menuHeight = 330; let x = e.clientX; let y = e.clientY; if (x + menuWidth > window.innerWidth - 10) { x = Math.max(10, e.clientX - menuWidth); } if (y + menuHeight > window.innerHeight - 10) { y = Math.max(10, window.innerHeight - menuHeight - 10); } el.userContextMenu.style.left = `${x}px`; el.userContextMenu.style.top = `${y}px`; } function closeUserContextMenu() { if (el.userContextMenu) { el.userContextMenu.classList.add('hidden'); } activeContextUser = null; } function setupUserContextMenu() { if (!el.userContextMenu) return; el.userContextMenu.addEventListener('click', (e) => { const btn = e.target.closest('.ctx-item'); if (!btn) return; const action = btn.dataset.action; if (!action || !activeContextUser) return; const nick = activeContextUser.nick; const chan = state.currentChannel; const tauri = getTauri(); closeUserContextMenu(); switch (action) { case 'query': addChannelToState(nick); switchChannel(nick); el.chatInput.focus(); break; case 'whois': if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `WHOIS ${nick} ${nick}` }); addMessage(chan, { type: 'system', text: `ℹ [WHOIS] Frage Benutzerinfo für "${nick}" ab...` }); } break; case 'grussbox': openGrussboxModal(); if (el.gbTextInput) { el.gbTextInput.value = `Liebe Grüße an ${nick}! `; el.gbTextInput.focus(); } break; case 'copy': navigator.clipboard.writeText(nick).then(() => { addMessage(chan, { type: 'system', text: `📋 [Clipboard] Nickname "${nick}" kopiert.` }); }); break; case 'op': if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `MODE ${chan} +o ${nick}` }); } break; case 'deop': if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `MODE ${chan} -o ${nick}` }); } break; case 'voice': if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `MODE ${chan} +v ${nick}` }); } break; case 'devoice': if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `MODE ${chan} -v ${nick}` }); } break; case 'kick': const reason = prompt(`Grund für den Kick von ${nick}:`, 'Vom Moderator gekickt'); if (reason !== null && state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `KICK ${chan} ${nick} :${reason}` }); } break; case 'ban': if (confirm(`Möchtest du ${nick} wirklich bannen und kicken?`)) { if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `MODE ${chan} +b ${nick}!*@*` }); tauri.core.invoke('send_irc_raw', { line: `KICK ${chan} ${nick} :Gebannt` }); } } break; } }); document.addEventListener('click', (e) => { if (!el.userContextMenu.classList.contains('hidden') && !el.userContextMenu.contains(e.target)) { closeUserContextMenu(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeUserContextMenu(); } }); } // --- Sending Messages --- async function sendMessage() { const text = el.chatInput.value.trim(); if (!text) return; el.chatInput.value = ''; if (el.emotePicker && !el.emotePicker.classList.contains('hidden')) { el.emotePicker.classList.add('hidden'); } if (text.startsWith('/')) { handleSlashCommand(text); return; } const tauri = getTauri(); if (!state.connected && tauri) { addMessage(state.currentChannel, { type: 'system', text: '❌ Nicht verbunden. Bitte in den Optionen -> Server verbinden.' }); return; } if (state.currentChannel === '*server*') { addMessage('*server*', { type: 'system', text: `>>> ${text}` }); if (tauri && tauri.core) { try { await tauri.core.invoke('send_irc_raw', { line: text }); } catch (err) { addMessage('*server*', { type: 'system', text: `Fehler beim Senden: ${err}` }); } } return; } addMessage(state.currentChannel, { nick: state.currentNick || 'Ich', text: text, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }); if (tauri && tauri.core) { try { await tauri.core.invoke('send_irc_message', { target: state.currentChannel, text: text }); } catch (err) { addMessage(state.currentChannel, { type: 'system', text: `Fehler beim Senden: ${err}` }); } } } function handleSlashCommand(raw) { const parts = raw.split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); const tauri = getTauri(); if (cmd === '/join' || cmd === '/j') { if (args[0]) { const target = args[0].startsWith('#') ? args[0] : '#' + args[0]; addChannelToState(target); switchChannel(target); if (tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `JOIN ${target}` }); } } } else if (cmd === '/part' || cmd === '/p') { const target = args[0] || state.currentChannel; if (tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `PART ${target}` }); } } else if (cmd === '/nick') { if (args[0] && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `NICK ${args[0]}` }); } } else if (cmd === '/me') { if (args.length > 0) { const actionText = args.join(' '); addMessage(state.currentChannel, { nick: state.currentNick || 'Ich', text: actionText, type: 'action' }); if (tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `PRIVMSG ${state.currentChannel} :\x01ACTION ${actionText}\x01` }); } } } else if (cmd === '/clones') { addMessage(state.currentChannel, { type: 'system', text: '🔍 Scanne Channel auf Klone (*!*@host)...' }); if (tauri && tauri.core) { tauri.core.invoke('scan_clones', { channel: state.currentChannel }); } } else if (cmd === '/clear') { if (state.channels[state.currentChannel]) { state.channels[state.currentChannel].messages = []; renderCurrentChannelMessages(); } } else { if (tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: raw.slice(1) }); } } } // --- Tauri Event Listeners & Bridge --- function setupTauriBridge() { const tauri = getTauri(); if (!tauri || !tauri.event) { console.warn('Tauri event module nicht verfügbar'); return; } const { listen } = tauri.event; listen('irc-status', (event) => { const { connected, connecting, nick, host } = event.payload; state.connected = connected; state.connecting = connecting; if (nick) { state.currentNick = nick; // Waehrend des Sendebetriebs ist der Nick der Modi-Nick, nicht der normale Nick. if (!state.isModiActive) { currentSettings.normalNick = nick; } } el.statusDot.className = 'status-dot ' + (connected ? 'connected' : (connecting ? 'connecting' : 'disconnected')); if (el.statusDot) { el.statusDot.title = connected ? `Verbunden (${host || 'irc.euer.tv'})` : (connecting ? 'Verbinde...' : 'Getrennt'); } if (el.statusText) { el.statusText.textContent = connected ? `Verbunden (${host})` : (connecting ? 'Verbinde...' : 'Getrennt'); } el.displayNick.textContent = state.currentNick || 'Nicht verbunden'; if (el.displayHost) { el.displayHost.textContent = host || 'irc.euer.tv'; } el.userAvatar.textContent = state.currentNick ? state.currentNick[0].toUpperCase() : '?'; if (el.btnHeaderConnect) { if (connected) { el.btnHeaderConnect.textContent = 'Trennen'; el.btnHeaderConnect.className = 'btn-connect-header connected'; } else if (connecting) { el.btnHeaderConnect.textContent = 'Verbinde...'; el.btnHeaderConnect.className = 'btn-connect-header'; } else { el.btnHeaderConnect.textContent = 'Verbinden'; el.btnHeaderConnect.className = 'btn-connect-header'; if (el.statusPing) el.statusPing.classList.add('hidden'); } } }); listen('update-progress', (event) => { if (el.updProgressBar) el.updProgressBar.style.width = `${event.payload}%`; }); listen('irc-ping', (event) => { const { ping_ms } = event.payload; if (!state.connected || !el.statusPing) return; el.statusPing.textContent = `${ping_ms} ms`; el.statusPing.classList.remove('hidden'); el.statusPing.className = 'status-ping' + (ping_ms > 150 ? ' ping-high' : (ping_ms > 60 ? ' ping-medium' : '')); }); listen('irc-message', (event) => { const { channel, nick, text, time, type } = event.payload; const target = channel || state.currentChannel; addMessage(target, { nick, text, time, type: type || 'normal' }); }); listen('irc-names', (event) => { const { channel, users } = event.payload; setUserList(channel, users); }); listen('irc-topic', (event) => { const { channel, topic } = event.payload; if (state.channels[channel]) { state.channels[channel].topic = topic; if (state.currentChannel === channel && el.headerTopic) { el.headerTopic.textContent = topic; } } }); listen('irc-channel-joined', (event) => { const { channel } = event.payload; addChannelToState(channel); switchChannel(channel); }); listen('irc-channel-parted', (event) => { const { channel } = event.payload; if (state.channels[channel] && channel !== '#erde') { delete state.channels[channel]; if (state.currentChannel === channel) { switchChannel('#erde'); } else { updateChannelListUI(); } } }); } function sanitizeIrcNick(raw) { let s = (raw || '').trim() .replace(/ä/g, 'ae').replace(/Ä/g, 'Ae') .replace(/ö/g, 'oe').replace(/Ö/g, 'Oe') .replace(/ü/g, 'ue').replace(/Ü/g, 'Ue') .replace(/ß/g, 'ss'); s = s.replace(/[^a-zA-Z0-9_\-\[\]\\^{}|]/g, '_'); return s || `Hoerer_${Math.floor(Math.random() * 900 + 100)}`; } // --- Connect Actions --- function showServerSettings(hint) { el.settingsModal.classList.remove('hidden'); const serverTab = document.querySelector('.modal-tabs .tab-btn[data-tab-target="secServer"]'); if (serverTab) serverTab.click(); setConnectionTab('direct'); if (el.directNick) el.directNick.focus(); if (hint) { addMessage('*server*', { type: 'system', text: `ℹ ${hint}` }); } } async function connectDirect() { await saveDirectSettings(false); const { host, port, channel, ssl } = RADIO_IRC; const nick = sanitizeIrcNick(currentSettings.directNick || ''); const pass = currentSettings.directPass || ''; if (!currentSettings.directNick) { showServerSettings('Bitte zuerst einen Namen eingeben.'); return; } state.currentNick = nick; currentSettings.directNick = nick; currentSettings.normalNick = nick; if (el.directNick) el.directNick.value = nick; if (el.cfgNormalNick) el.cfgNormalNick.value = nick; if (el.displayNick) el.displayNick.textContent = nick; if (el.userAvatar) el.userAvatar.textContent = nick[0].toUpperCase(); el.settingsModal.classList.add('hidden'); el.statusDot.className = 'status-dot connecting'; if (el.statusDot) el.statusDot.title = `Verbinde mit ${host}:${port}...`; if (el.statusText) el.statusText.textContent = `Verbinde mit ${host}:${port}...`; if (el.btnHeaderConnect) { el.btnHeaderConnect.textContent = 'Verbinde...'; el.btnHeaderConnect.className = 'btn-connect-header'; } addMessage('*server*', { type: 'system', text: `Verbindungsaufbau zu ${host}:${port} (SSL: ${ssl ? 'AN' : 'AUS'}), Nick: ${nick}...` }); const tauri = getTauri(); if (tauri && tauri.core) { try { await tauri.core.invoke('connect_irc', { config: { host, port, nick, channel, // Das Passwort wird als NickServ-Identify gesendet (kein Server-PASS). password: null, use_ssl: ssl, accept_invalid_certs: false, nickserv_passwords: pass ? { ...(currentSettings.nickservPasswords || {}), [nick.toLowerCase()]: pass } : (currentSettings.nickservPasswords || null) } }); } catch (err) { addMessage('*server*', { type: 'system', text: `❌ Verbindungsfehler: ${err}` }); } } } async function connectZnc() { const host = (el.zncHost && el.zncHost.value.trim()) || currentSettings.zncHost; const port = parseInt(el.zncPort ? el.zncPort.value : '8250', 10) || currentSettings.zncPort || 8250; const user = (el.zncUser && el.zncUser.value.trim()) || currentSettings.zncUser; const pass = (el.zncPass && el.zncPass.value.trim()) || currentSettings.zncPass; const nick = (el.zncNick && el.zncNick.value.trim()) || currentSettings.zncNick || (user ? user.split('/')[0] : 'Hoerer'); const channel = (el.zncChannel && el.zncChannel.value.trim()) || currentSettings.zncChannel || '#erde'; const ssl = el.zncSsl ? el.zncSsl.checked : !!currentSettings.zncSsl; if (!host || !user || !pass) { alert('Bitte ZNC Host, ZNC-Benutzer und Passwort ausfüllen.'); return; } await saveZncSettings(false); state.currentNick = nick; currentSettings.normalNick = nick; if (el.displayNick) el.displayNick.textContent = nick; if (el.userAvatar) el.userAvatar.textContent = nick[0].toUpperCase(); el.settingsModal.classList.add('hidden'); el.statusDot.className = 'status-dot connecting'; if (el.statusDot) el.statusDot.title = `Verbinde mit ZNC ${host}:${port}...`; if (el.statusText) el.statusText.textContent = `Verbinde mit ZNC ${host}:${port}...`; if (el.btnHeaderConnect) { el.btnHeaderConnect.textContent = 'Verbinde...'; el.btnHeaderConnect.className = 'btn-connect-header'; } addMessage('*server*', { type: 'system', text: `ZNC Bouncer Verbindung zu ${host}:${port} für Benutzer ${user}...` }); const tauri = getTauri(); if (tauri && tauri.core) { try { await tauri.core.invoke('connect_irc', { config: { host, port, nick, channel, password: `${user}:${pass}`, use_ssl: ssl, accept_invalid_certs: currentSettings.acceptInvalidCerts === true, nickserv_passwords: currentSettings.nickservPasswords || null } }); } catch (err) { addMessage('*server*', { type: 'system', text: `❌ ZNC Verbindungsfehler: ${err}` }); } } } async function disconnectIrc() { const tauri = getTauri(); if (tauri && tauri.core) { try { await tauri.core.invoke('disconnect_irc'); } catch (err) { console.warn('Disconnect error:', err); } } state.connected = false; state.connecting = false; el.statusDot.className = 'status-dot disconnected'; if (el.statusDot) el.statusDot.title = 'Getrennt'; if (el.statusText) el.statusText.textContent = 'Getrennt'; if (el.statusPing) el.statusPing.classList.add('hidden'); if (el.btnHeaderConnect) { el.btnHeaderConnect.textContent = 'Verbinden'; el.btnHeaderConnect.className = 'btn-connect-header'; } } // --- Livestream & Radio Player --- const streamQualityMap = { '1080p': { primary: 'https://hls21.euer.tv/hls/1080p.m3u8', fallback: 'https://hls23.euer.tv/hls/1080p.m3u8' }, '720p': { primary: 'https://hls21.euer.tv/hls/720p.m3u8', fallback: 'https://hls23.euer.tv/hls/720p.m3u8' }, '360p': { primary: 'https://hls21.euer.tv/hls/360p.m3u8', fallback: 'https://hls23.euer.tv/hls/360p.m3u8' } }; const DEFAULT_STREAM_VOLUME = 0.10; // 10% Standardlautstärke let hlsInstance = null; let metadataTimer = null; let currentTrackTitle = ''; let currentQuality = localStorage.getItem('euerchat_video_quality') || '1080p'; function initStreamPlayer() { if (currentSettings.showMediaArea === false) { if (el.sidebarMediaArea) el.sidebarMediaArea.classList.add('hidden'); } else { startStreamPlayer(); } // Start metadata polling fetchStreamMetadata(); if (metadataTimer) clearInterval(metadataTimer); metadataTimer = setInterval(fetchStreamMetadata, 10000); } function startStreamPlayer() { const video = el.streamVideo; if (!video) return; // IMMER Stumm beim Start mit 10% Standardlautstärke video.muted = true; const storedVol = parseFloat(localStorage.getItem('euerchat_volume') || '0.10'); const vol = storedVol > 0 ? storedVol : DEFAULT_STREAM_VOLUME; video.volume = vol; if (el.streamVolSlider) el.streamVolSlider.value = vol; if (el.btnStreamMute) el.btnStreamMute.classList.add('is-muted'); if (el.streamMuteIcon) el.streamMuteIcon.textContent = '🔇'; if (el.videoQualitySelect) { if (streamQualityMap[currentQuality]) { el.videoQualitySelect.value = currentQuality; } else { currentQuality = '1080p'; el.videoQualitySelect.value = '1080p'; } } loadStreamQuality(currentQuality); } function stopStreamPlayer() { if (hlsInstance) { try { hlsInstance.stopLoad(); hlsInstance.destroy(); } catch (e) {} hlsInstance = null; } const video = el.streamVideo; if (video) { video.pause(); video.removeAttribute('src'); video.load(); } } function loadStreamQuality(quality) { const video = el.streamVideo; if (!video) return; if (!streamQualityMap[quality]) quality = '1080p'; currentQuality = quality; localStorage.setItem('euerchat_video_quality', quality); const urls = streamQualityMap[quality]; if (window.Hls && window.Hls.isSupported()) { if (hlsInstance) { try { hlsInstance.destroy(); } catch (e) {} } hlsInstance = new window.Hls({ autoStartLoad: true, startPosition: -1, capLevelToPlayerSize: false, liveSyncDurationCount: 3 }); hlsInstance.loadSource(urls.primary); hlsInstance.attachMedia(video); hlsInstance.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal) { switch (data.type) { case window.Hls.ErrorTypes.NETWORK_ERROR: console.warn('[HLS] Network error, trying fallback server...'); if (hlsInstance.url === urls.primary) { hlsInstance.loadSource(urls.fallback); } else { hlsInstance.startLoad(); } break; case window.Hls.ErrorTypes.MEDIA_ERROR: console.warn('[HLS] Media error, recovering...'); hlsInstance.recoverMediaError(); break; default: console.warn('[HLS] Fatal error, reloading stream...'); try { hlsInstance.destroy(); } catch (e) {} setTimeout(() => loadStreamQuality(currentQuality), 3000); break; } } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = urls.primary; } video.play().catch(err => { console.log('Video autoplay:', err); }); } function toggleStreamMute() { const video = el.streamVideo; if (!video) return; if (video.muted || video.volume === 0) { // Unmute: Standardwert 10% falls Lautstärke 0 ist video.muted = false; const stored = parseFloat(localStorage.getItem('euerchat_volume') || '0.10'); const targetVol = stored > 0 ? stored : DEFAULT_STREAM_VOLUME; video.volume = targetVol; if (el.streamVolSlider) el.streamVolSlider.value = targetVol; if (el.btnStreamMute) el.btnStreamMute.classList.remove('is-muted'); if (el.streamMuteIcon) el.streamMuteIcon.textContent = targetVol > 0.5 ? '🔊' : '🔉'; } else { // Mute video.muted = true; if (el.btnStreamMute) el.btnStreamMute.classList.add('is-muted'); if (el.streamMuteIcon) el.streamMuteIcon.textContent = '🔇'; } } function handleVolumeChange(e) { const video = el.streamVideo; if (!video) return; const val = parseFloat(e.target.value); video.volume = val; localStorage.setItem('euerchat_volume', val); if (val === 0) { video.muted = true; if (el.btnStreamMute) el.btnStreamMute.classList.add('is-muted'); if (el.streamMuteIcon) el.streamMuteIcon.textContent = '🔇'; } else { video.muted = false; if (el.btnStreamMute) el.btnStreamMute.classList.remove('is-muted'); if (el.streamMuteIcon) el.streamMuteIcon.textContent = val > 0.5 ? '🔊' : '🔉'; } } function resyncStream() { if (hlsInstance) { hlsInstance.stopLoad(); hlsInstance.startLoad(-1); } if (el.streamVideo) { el.streamVideo.load(); el.streamVideo.play().catch(() => {}); } fetchStreamMetadata(); } async function fetchStreamMetadata() { if (document.hidden) return; try { const res = await fetch('https://api.euer.tv/v2/getjsondata.php?status'); if (!res.ok) return; const data = await res.json(); let artist = data.artist || ''; let track = data.track || ''; if (!artist && !track && data.metadata) { if (data.metadata.includes(' - ')) { const parts = data.metadata.split(' - '); artist = parts[0].trim(); track = parts.slice(1).join(' - ').trim(); } else { artist = 'euer.tv Radio'; track = data.metadata; } } if (!artist) artist = 'euer.tv Radio'; if (!track) track = 'Radio & Video Livestream'; const fullTitle = `${artist} - ${track}`; if (fullTitle !== currentTrackTitle) { currentTrackTitle = fullTitle; if (el.sidebarArtist) el.sidebarArtist.textContent = artist; if (el.sidebarTrack) el.sidebarTrack.textContent = track; adjustTrackTextSizes(); } // Update listener count in sidebar media header if (data.listeners && data.listeners[0]) { const cur = data.listeners[0].current || '0'; const vid = data.listeners[0].video || '0'; const aud = data.listeners[0].audio || '0'; const num = parseInt(cur, 10); const formatted = isNaN(num) ? cur : num.toLocaleString('de-DE'); if (el.statTotalListeners) el.statTotalListeners.textContent = formatted; if (el.statVideoListeners) el.statVideoListeners.textContent = vid; if (el.statAudioListeners) el.statAudioListeners.textContent = aud; } } catch (err) { // Ignore network hiccups } } // --- Dynamic Text Auto-Shrink (Automatisches Verkleinern bei langen Titeln) --- function adjustTrackTextSizes() { fitText(el.sidebarArtist, 15, 10); fitText(el.sidebarTrack, 13, 9.5); } function fitText(element, maxPx, minPx) { if (!element) return; const parent = element.parentElement; if (!parent) return; // Reset to maximum size first to measure correctly element.style.fontSize = `${maxPx}px`; const availWidth = parent.clientWidth; if (!availWidth) return; // Dynamically shrink if content overflows let curSize = maxPx; while (element.scrollWidth > availWidth && curSize > minPx) { curSize -= 0.5; element.style.fontSize = `${curSize}px`; } } // --- Grußbox Handler --- function openGrussboxModal() { if (!el.grussboxModal) return; el.grussboxModal.classList.remove('hidden'); if (el.gbNameInput) { el.gbNameInput.value = state.currentNick || ''; } if (el.gbTextInput) { el.gbTextInput.value = ''; setTimeout(() => { if (el.gbTextInput) el.gbTextInput.focus(); }, 80); } if (el.gbFeedback) { el.gbFeedback.className = 'grussbox-feedback hidden'; el.gbFeedback.textContent = ''; } if (el.btnSubmitGruss) { el.btnSubmitGruss.disabled = false; el.btnSubmitGruss.textContent = '🚀 Gruß senden'; } } function closeGrussboxModal() { if (el.grussboxModal) { el.grussboxModal.classList.add('hidden'); } } async function submitGrussbox() { const name = el.gbNameInput ? el.gbNameInput.value.trim() : ''; const text = el.gbTextInput ? el.gbTextInput.value.trim() : ''; if (!name) { showGrussboxFeedback('Bitte gib deinen Namen ein.', 'error'); if (el.gbNameInput) el.gbNameInput.focus(); return; } if (!text) { showGrussboxFeedback('Bitte gib einen Gruß- oder Wunschtext ein.', 'error'); if (el.gbTextInput) el.gbTextInput.focus(); return; } const tauri = getTauri(); if (!tauri || !tauri.core) { showGrussboxFeedback('Tauri API nicht verfügbar.', 'error'); return; } if (el.btnSubmitGruss) { el.btnSubmitGruss.disabled = true; el.btnSubmitGruss.textContent = '⏳ Wird gesendet...'; } try { const resp = await tauri.core.invoke('send_grussbox', { name, text }); showGrussboxFeedback('✅ Gruß erfolgreich an das Studio übermittelt!', 'success'); if (el.gbTextInput) el.gbTextInput.value = ''; addMessage(state.currentChannel, { type: 'system', text: `✉ [Grußbox] Gruß von "${name}" wurde an das Studio übermittelt.` }); setTimeout(() => { closeGrussboxModal(); }, 1600); } catch (err) { showGrussboxFeedback(`❌ Fehler beim Senden: ${err}`, 'error'); if (el.btnSubmitGruss) { el.btnSubmitGruss.disabled = false; el.btnSubmitGruss.textContent = '🚀 Gruß senden'; } } } function showGrussboxFeedback(msg, type) { if (!el.gbFeedback) return; el.gbFeedback.textContent = msg; el.gbFeedback.className = `grussbox-feedback ${type}`; } // --- Draggable Sidebar Resizer --- function setupSidebarResizer() { const resizer = el.sidebarResizer; const sidebar = el.sidebarUsers; if (!resizer || !sidebar) return; // Restore saved width from localStorage (generous limit so user can resize stream as wide as desired) const savedWidth = localStorage.getItem('euerchat_sidebar_width'); const maxAllowed = Math.max(300, window.innerWidth - 260); if (savedWidth) { const parsed = parseInt(savedWidth, 10); if (parsed >= 160 && parsed <= maxAllowed) { sidebar.style.width = `${parsed}px`; } } let isResizing = false; resizer.addEventListener('mousedown', (e) => { e.preventDefault(); isResizing = true; document.body.classList.add('resizing'); const onMouseMove = (moveEvent) => { if (!isResizing) return; const currentMax = Math.max(300, window.innerWidth - 260); const newWidth = window.innerWidth - moveEvent.clientX; if (newWidth >= 160 && newWidth <= currentMax) { sidebar.style.width = `${newWidth}px`; adjustTrackTextSizes(); } }; const onMouseUp = () => { if (isResizing) { isResizing = false; document.body.classList.remove('resizing'); const currentWidth = parseInt(sidebar.style.width, 10); if (currentWidth) { localStorage.setItem('euerchat_sidebar_width', currentWidth); } adjustTrackTextSizes(); window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); } }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }); window.addEventListener('resize', adjustTrackTextSizes); } // --- System Fonts Loading --- async function loadSystemFonts() { const select = el.optFontFamily; if (!select) return; try { const tauri = getTauri(); if (tauri && tauri.core) { const fonts = await tauri.core.invoke('get_installed_fonts'); if (Array.isArray(fonts) && fonts.length > 0) { const curVal = currentSettings.fontFamily || 'system'; select.innerHTML = ''; const optSystem = document.createElement('option'); optSystem.value = 'system'; optSystem.textContent = 'Standard Sans (Segoe UI, System)'; select.appendChild(optSystem); // Popular Fonts group const optGroupPop = document.createElement('optgroup'); optGroupPop.label = '★ Beliebte Chat-Schriftarten'; const populars = ['Segoe UI', 'Consolas', 'Arial', 'Calibri', 'Comic Sans MS', 'Verdana', 'Trebuchet MS', 'Courier New', 'Georgia', 'Impact']; for (const f of populars) { const opt = document.createElement('option'); opt.value = f; opt.textContent = f; optGroupPop.appendChild(opt); } select.appendChild(optGroupPop); // All Windows Installed Fonts const optGroupAll = document.createElement('optgroup'); optGroupAll.label = `Windows Installiert (${fonts.length} Schriftarten)`; for (const f of fonts) { const opt = document.createElement('option'); opt.value = f; opt.textContent = f; optGroupAll.appendChild(opt); } select.appendChild(optGroupAll); select.value = curVal; } } } catch (e) { console.warn('System-Schriftarten konnten nicht geladen werden:', e); } } // --- Updates (Gitea-Releases) --- let updateInstalling = false; function setUpdateStatus(target, text, kind) { if (!target) return; target.textContent = text; target.className = 'update-status' + (kind ? ` ${kind}` : ''); } async function loadAppVersion() { const tauri = getTauri(); if (!tauri || !tauri.core || !el.updCurrentVersion) return; try { el.updCurrentVersion.textContent = await tauri.core.invoke('get_app_version'); } catch (e) { /* Anzeige bleibt leer */ } } function showUpdateDialog(info) { if (!el.updateModal) return; el.updSummary.textContent = `${info.name || 'EuerChat ' + info.latest} ist verfügbar (installiert: ${info.current}). Möchtest du jetzt aktualisieren?`; // Release-Notizen nur als Text anzeigen, nie als HTML. el.updNotes.textContent = (info.notes || '').trim() || 'Für dieses Update wurden keine Release-Notizen hinterlegt.'; el.updProgress.classList.add('hidden'); el.updProgressBar.style.width = '0'; setUpdateStatus(el.updModalStatus, '', ''); el.btnUpdateInstall.disabled = false; el.btnUpdateLater.disabled = false; el.updateModal.classList.remove('hidden'); } async function checkForUpdates(manual) { const tauri = getTauri(); if (!tauri || !tauri.core || updateInstalling) return; if (manual) { if (el.btnCheckUpdate) el.btnCheckUpdate.disabled = true; setUpdateStatus(el.updStatusText, 'Suche nach Updates …', ''); } try { const info = await tauri.core.invoke('check_for_update'); if (info.available) { setUpdateStatus(el.updStatusText, `Version ${info.latest} ist verfügbar.`, 'success'); showUpdateDialog(info); } else if (manual) { setUpdateStatus(el.updStatusText, `✓ EuerChat ${info.current} ist aktuell.`, 'success'); } } catch (err) { // Der automatische Start-Check soll bei Netzproblemen nicht stören. if (manual) { setUpdateStatus(el.updStatusText, `❌ ${err}`, 'error'); } else { console.warn('Update-Prüfung fehlgeschlagen:', err); } } finally { if (manual && el.btnCheckUpdate) el.btnCheckUpdate.disabled = false; } } async function installUpdate() { const tauri = getTauri(); if (!tauri || !tauri.core || updateInstalling) return; updateInstalling = true; el.btnUpdateInstall.disabled = true; el.btnUpdateLater.disabled = true; el.updProgress.classList.remove('hidden'); setUpdateStatus(el.updModalStatus, 'Update wird geladen und geprüft …', ''); try { await tauri.core.invoke('install_update'); setUpdateStatus(el.updModalStatus, 'Installation läuft. EuerChat startet gleich neu …', 'success'); } catch (err) { updateInstalling = false; el.btnUpdateInstall.disabled = false; el.btnUpdateLater.disabled = false; setUpdateStatus(el.updModalStatus, `❌ ${err}`, 'error'); } } function closeUpdateDialog() { if (!updateInstalling && el.updateModal) el.updateModal.classList.add('hidden'); } // --- Event Listeners Setup --- function setupEventListeners() { el.btnSend.addEventListener('click', sendMessage); el.chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { sendMessage(); } }); // Nickname real-time sync if (el.directNick) { el.directNick.addEventListener('input', (e) => { const val = e.target.value.trim(); if (val) { currentSettings.directNick = val; currentSettings.normalNick = val; if (el.cfgNormalNick) el.cfgNormalNick.value = val; } }); } // Header Connect / Disconnect Button if (el.btnHeaderConnect) { el.btnHeaderConnect.addEventListener('click', () => { if (state.connected || state.connecting) { disconnectIrc(); } else { if (currentSettings.activeConnection === 'znc') { connectZnc(); } else { connectDirect(); } } }); } // Modi Switch Button if (el.btnModiToggle) { el.btnModiToggle.addEventListener('click', toggleModiState); } // Updates if (el.btnCheckUpdate) el.btnCheckUpdate.addEventListener('click', () => checkForUpdates(true)); if (el.btnUpdateInstall) el.btnUpdateInstall.addEventListener('click', installUpdate); if (el.btnUpdateLater) el.btnUpdateLater.addEventListener('click', closeUpdateDialog); if (el.btnCloseUpdate) el.btnCloseUpdate.addEventListener('click', closeUpdateDialog); el.btnEmotePicker.addEventListener('click', (e) => { e.stopPropagation(); el.emotePicker.classList.toggle('hidden'); if (!el.emotePicker.classList.contains('hidden')) { el.emoteSearch.focus(); } }); el.btnCloseEmote.addEventListener('click', () => { el.emotePicker.classList.add('hidden'); }); // Close emote picker when clicking outside document.addEventListener('click', (e) => { if (el.emotePicker && !el.emotePicker.classList.contains('hidden')) { if (!el.emotePicker.contains(e.target) && !el.btnEmotePicker.contains(e.target)) { el.emotePicker.classList.add('hidden'); } } }); // Close emote picker on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && el.emotePicker && !el.emotePicker.classList.contains('hidden')) { el.emotePicker.classList.add('hidden'); } }); el.emoteSearch.addEventListener('input', (e) => { const activeTab = document.querySelector('.emote-tab.active')?.dataset.tab || 'all'; renderEmoteGrid(activeTab, e.target.value); }); el.emoteTabs.forEach(tab => { tab.addEventListener('click', () => { el.emoteTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderEmoteGrid(tab.dataset.tab, el.emoteSearch.value); }); }); // Settings Modal el.btnSettingsModal.addEventListener('click', () => { el.settingsModal.classList.remove('hidden'); updatePreview(); }); el.btnCloseSettingsModal.addEventListener('click', () => { el.settingsModal.classList.add('hidden'); }); el.btnSaveSettings.addEventListener('click', saveSettings); el.btnResetSettings.addEventListener('click', () => { applySettings(defaultSettings); }); // Modal Tab Switching document.querySelectorAll('.modal-tabs .tab-btn').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.modal-tabs .tab-btn').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const targetId = tab.dataset.tabTarget; document.querySelectorAll('.settings-section-pane').forEach(pane => { if (pane.id === targetId) { pane.classList.remove('hidden'); pane.classList.add('active'); } else { pane.classList.add('hidden'); pane.classList.remove('active'); } }); }); }); // Server Form Type Switcher (Direct vs ZNC) if (el.btnTypeDirect) { el.btnTypeDirect.addEventListener('click', () => setConnectionTab('direct')); } if (el.btnTypeZnc) { el.btnTypeZnc.addEventListener('click', () => setConnectionTab('znc')); } // Direct Form Actions if (el.btnSaveDirect) { el.btnSaveDirect.addEventListener('click', () => saveDirectSettings(true)); } if (el.btnConnectNow) { el.btnConnectNow.addEventListener('click', connectDirect); } if (el.btnClearDirect) { el.btnClearDirect.addEventListener('click', clearDirectForm); } // ZNC Form Actions if (el.btnSaveZnc) { el.btnSaveZnc.addEventListener('click', () => saveZncSettings(true)); } if (el.btnConnectZncNow) { el.btnConnectZncNow.addEventListener('click', connectZnc); } if (el.btnClearZnc) { el.btnClearZnc.addEventListener('click', clearZncForm); } // Live Settings Inputs Preview Listeners el.optFontSize.addEventListener('input', (e) => { el.valFontSize.textContent = `${e.target.value}px`; updatePreview(); }); if (el.optFontCustom) { el.optFontCustom.addEventListener('input', updatePreview); } [el.optFontFamily, el.optLineHeight, el.optEmoteSize].forEach(input => { if (input) input.addEventListener('change', updatePreview); }); if (el.optShowMediaArea) { el.optShowMediaArea.addEventListener('change', (e) => { const visible = e.target.checked; if (el.sidebarMediaArea) el.sidebarMediaArea.classList.toggle('hidden', !visible); if (visible) { startStreamPlayer(); } else { stopStreamPlayer(); } }); } // User Context Menu Event Listeners setupUserContextMenu(); // Lua checkbox toggle if (el.chkEnableLua) { el.chkEnableLua.addEventListener('change', (e) => { currentSettings.luaEnabled = e.target.checked; updateLuaUI(); }); } // Lua file input if (el.btnSelectLua) { el.btnSelectLua.addEventListener('click', () => { el.luaFileInput.click(); }); } if (el.luaFileInput) { el.luaFileInput.addEventListener('change', (e) => { if (e.target.files && e.target.files[0]) { const fn = e.target.files[0].name; currentSettings.luaEnabled = true; currentSettings.luaFileName = fn; if (el.luaFileName) el.luaFileName.textContent = fn; updateLuaUI(); addMessage('*server*', { type: 'system', text: `[Lua] Skript "${fn}" wurde manuell geladen und aktiviert.` }); } }); } // Livestream Audio & Video Controls if (el.btnStreamMute) { el.btnStreamMute.addEventListener('click', toggleStreamMute); } if (el.streamVolSlider) { el.streamVolSlider.addEventListener('input', handleVolumeChange); } if (el.videoQualitySelect) { el.videoQualitySelect.addEventListener('change', (e) => { loadStreamQuality(e.target.value); }); } if (el.btnResyncStream) { el.btnResyncStream.addEventListener('click', resyncStream); } // Grußbox Event Listeners if (el.btnOpenGrussbox) { el.btnOpenGrussbox.addEventListener('click', openGrussboxModal); } if (el.btnCloseGrussbox) { el.btnCloseGrussbox.addEventListener('click', closeGrussboxModal); } if (el.btnCancelGruss) { el.btnCancelGruss.addEventListener('click', closeGrussboxModal); } if (el.btnSubmitGruss) { el.btnSubmitGruss.addEventListener('click', submitGrussbox); } if (el.gbTextInput) { el.gbTextInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); submitGrussbox(); } }); } // Add channel prompt (+) el.btnAddChan.addEventListener('click', () => { const name = prompt('Kanalname eingeben (z.B. #stream):'); if (name) { const formatted = name.startsWith('#') ? name : '#' + name; addChannelToState(formatted); switchChannel(formatted); const tauri = getTauri(); if (state.connected && tauri && tauri.core) { tauri.core.invoke('send_irc_raw', { line: `JOIN ${formatted}` }); } } }); // Clean disconnect on window close window.addEventListener('beforeunload', () => { const tauri = getTauri(); if (tauri && tauri.core) { tauri.core.invoke('disconnect_irc'); } }); } // --- Startup --- window.addEventListener('DOMContentLoaded', async () => { await loadSettings(); loadEmotes(); setupEventListeners(); setupSidebarResizer(); setupTauriBridge(); initStreamPlayer(); loadSystemFonts(); loadAppVersion(); if (currentSettings.autoUpdateCheck !== false) { setTimeout(() => checkForUpdates(false), 4000); } setTimeout(adjustTrackTextSizes, 300); // Restore saved nickname (from active connection profile or fallback) const isZnc = currentSettings.activeConnection === 'znc'; const savedNick = (isZnc ? currentSettings.zncNick : currentSettings.directNick) || currentSettings.normalNick || ''; if (el.directNick && currentSettings.directNick) el.directNick.value = currentSettings.directNick; if (el.zncNick && currentSettings.zncNick) el.zncNick.value = currentSettings.zncNick; if (el.cfgNormalNick) el.cfgNormalNick.value = currentSettings.normalNick || savedNick; state.currentNick = savedNick; if (el.displayNick) el.displayNick.textContent = savedNick || 'Nicht verbunden'; if (el.userAvatar) el.userAvatar.textContent = savedNick ? savedNick[0].toUpperCase() : '?'; updateLuaUI(); // Erststart: Name abfragen, bevor irgendetwas verbunden wird if (!savedNick) { showServerSettings('Willkommen! Gib deinen Namen ein und klicke auf "Jetzt Verbinden".'); return; } // Auto-connect ONLY with saved profile if enabled if (isZnc) { if (currentSettings.zncAutoConnect) { connectZnc(); } } else { if (currentSettings.directAutoConnect) { connectDirect(); } } }); })();