"use strict"; /* ============================================================ * KONFIGURATION * ============================================================ */ const CONFIG = { /* * Your Jitsi Launcher domain */ jitsiLauncher: "meet.bnvb.eu", /* * Your Jitsi domain. */ jitsiHost: "meet.bnvb-ingenieure.de", /* * Timeout trying to start the jitsi-meet-electron client */ launchTimeout: 2000, /* * set wallpaper for webpage */ backgroundImage: "assets/background.png", /* * Set logo */ logo: "assets/logo.svg", /* * Download URLs for jitsi-meet-electron client */ downloads: { windows: "https://github.com/jitsi/jitsi-meet-electron/releases", macos: "https://github.com/jitsi/jitsi-meet-electron/releases", linux: "https://github.com/jitsi/jitsi-meet-electron/releases" } }; /* ============================================================ * Translations * ============================================================ */ /* * Sprachen liegen in assets/i18n.json und werden beim * Start asynchron geladen (siehe START). */ let TRANSLATIONS = {}; const I18N_URL = "assets/i18n.json"; /* ============================================================ * Detect language * ============================================================ */ function detectLanguage() { /* * Gespeicherte Sprachwahl hat Vorrang. */ try { const saved = localStorage.getItem("jitsiLauncherLanguage"); if (saved && TRANSLATIONS[saved]) { return saved; } } catch (error) { /* * localStorage kann z.B. im Privat-Modus blockiert sein. */ } const languages = navigator.languages || [navigator.language]; for (const language of languages) { if (!language) { continue; } const languageCode = language .toLowerCase() .split("-")[0]; if (TRANSLATIONS[languageCode]) { return languageCode; } } /* * Englisch ist der Standard/Fallback. */ return "en"; } let currentLanguage = "en"; let translation = {}; /* ============================================================ * Apply translations to webpage * ============================================================ */ function applyTranslations() { document.documentElement.lang = currentLanguage; if (translation.pageTitle) { document.title = translation.pageTitle; } document .querySelectorAll("[data-i18n]") .forEach((element) => { const key = element.dataset.i18n; const value = getTranslation(key); if (value !== null) { element.textContent = value; } }); } /* * Übersetzung anhand eines Pfades lesen. * * Beispiel: * * "downloads.windows.title" * * ergibt: * * translation.downloads.windows.title */ function getTranslation(key) { const parts = key.split("."); let value = translation; for (const part of parts) { if ( value === null || value === undefined || typeof value !== "object" || !(part in value) ) { return null; } value = value[part]; } return typeof value === "string" ? value : null; } /* ============================================================ * LANGUAGE MENU * ============================================================ */ const LANGUAGE_NAMES = { en: "English", de: "Deutsch", fr: "Français", zh: "中文" }; const LANGUAGE_FLAGS = { en: "assets/uk.svg", de: "assets/de.svg", fr: "assets/fr.svg", zh: "assets/cn.svg" }; function updateLanguageMenu() { langFlag.src = LANGUAGE_FLAGS[currentLanguage]; langLabel.textContent = LANGUAGE_NAMES[currentLanguage]; langDropdown .querySelectorAll("button[data-lang]") .forEach((item) => { item.classList.toggle( "active", item.dataset.lang === currentLanguage ); }); } function setLanguage(language) { if (!TRANSLATIONS[language]) { return; } currentLanguage = language; translation = TRANSLATIONS[language]; try { localStorage.setItem("jitsiLauncherLanguage", language); } catch (error) { /* * localStorage kann z.B. im Privat-Modus blockiert sein. */ } applyTranslations(); updateLanguageMenu(); } function toggleLanguageMenu() { const isOpen = langDropdown.classList.toggle("open"); langButton.setAttribute( "aria-expanded", isOpen ? "true" : "false" ); } function closeLanguageMenu() { langDropdown.classList.remove("open"); langButton.setAttribute("aria-expanded", "false"); } /* ============================================================ * NAV MENU (Hamburger) * ============================================================ */ function toggleNavMenu() { const isOpen = navActions.classList.toggle("open"); navToggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); } function closeNavMenu() { navActions.classList.remove("open"); navToggle.setAttribute("aria-expanded", "false"); } /* ============================================================ * THEME * ============================================================ */ const THEME_KEY = "jitsiLauncherTheme"; function savedTheme() { try { const saved = localStorage.getItem(THEME_KEY); if (saved === "dark" || saved === "light") { return saved; } } catch (error) { /* * localStorage kann z.B. im Privat-Modus blockiert sein. */ } return null; } function systemIsDark() { return window.matchMedia("(prefers-color-scheme: dark)").matches; } function detectTheme() { /* * Gespeicherte Wahl hat Vorrang, sonst System-Einstellung. */ return savedTheme() || (systemIsDark() ? "dark" : "light"); } let currentTheme = detectTheme(); function updateThemeToggle() { themeIconSun.classList.toggle("hidden", currentTheme !== "dark"); themeIconMoon.classList.toggle("hidden", currentTheme === "dark"); } function applyTheme() { document.documentElement.classList.toggle("dark", currentTheme === "dark"); updateThemeToggle(); } function toggleTheme() { currentTheme = currentTheme === "dark" ? "light" : "dark"; try { localStorage.setItem(THEME_KEY, currentTheme); } catch (error) { /* * localStorage kann z.B. im Privat-Modus blockiert sein. */ } applyTheme(); } /* ============================================================ * BACKGROUND IMAGE * ============================================================ */ function applyBackgroundImage() { if (!CONFIG.backgroundImage) { return; } document.body.style.backgroundImage = `url("${CONFIG.backgroundImage}")`; document.body.style.backgroundSize = "cover"; document.body.style.backgroundPosition = "center"; } /* ============================================================ * LOGO * ============================================================ */ function applyLogo() { if (!CONFIG.logo) { return; } logoImage.src = CONFIG.logo; logoBar.classList.remove("hidden"); } /* ============================================================ * DOM elements * ============================================================ */ const launcher = document.getElementById("launcher"); const starting = document.getElementById("starting"); const fallback = document.getElementById("fallback"); const invalidRoom = document.getElementById("invalidRoom"); const meetingCreator = document.getElementById("meetingCreator"); const roomName = document.getElementById("roomName"); const openButton = document.getElementById("openButton"); const retryButton = document.getElementById("retryButton"); const browserLink = document.getElementById("browserLink"); const windowsDownload = document.getElementById("windowsDownload"); const macosDownload = document.getElementById("macosDownload"); const linuxDownload = document.getElementById("linuxDownload"); const otherDownload = document.getElementById("otherDownload"); const windowsLink = document.getElementById("windowsLink"); const macosLink = document.getElementById("macosLink"); const linuxLink = document.getElementById("linuxLink"); const createNewMeetingButton = document.getElementById("createNewMeetingButton"); const createNewFromLauncherButton = document.getElementById("createNewFromLauncherButton"); /* Meeting Creator DOM elements */ const meetingNameInput = document.getElementById("meetingName"); const meetingDescriptionInput = document.getElementById("meetingDescription"); const meetingStartInput = document.getElementById("meetingStart"); const meetingEndInput = document.getElementById("meetingEnd"); const meetingOrganizerInput = document.getElementById("meetingOrganizer"); const createMeetingButton = document.getElementById("createMeetingButton"); const createAdhocButton = document.getElementById("createAdhocButton"); const cancelCreateButton = document.getElementById("cancelCreateButton"); const icsSuccess = document.getElementById("icsSuccess"); const downloadIcsLink = document.getElementById("downloadIcsLink"); const adhocResult = document.getElementById("adhocResult"); const adhocLink = document.getElementById("adhocLink"); const copyAdhocLinkButton = document.getElementById("copyAdhocLinkButton"); /* Navbar DOM elements */ const navbar = document.querySelector(".navbar"); const navToggle = document.getElementById("navToggle"); const navActions = document.getElementById("navActions"); /* Language menu DOM elements */ const langMenu = document.getElementById("langMenu"); const langButton = document.getElementById("langButton"); const langDropdown = document.getElementById("langDropdown"); const langFlag = document.getElementById("langFlag"); const langLabel = document.getElementById("langLabel"); /* Invalid room view DOM elements */ const missingRoomTitle = document.getElementById("missingRoomTitle"); const missingRoomError = document.getElementById("missingRoomError"); const invalidNameTitle = document.getElementById("invalidNameTitle"); const invalidNameError = document.getElementById("invalidNameError"); const invalidRoomName = document.getElementById("invalidRoomName"); /* Logo DOM elements */ const logoBar = document.getElementById("logoBar"); const logoImage = document.getElementById("logoImage"); /* Theme DOM elements */ const themeButton = document.getElementById("themeButton"); const themeIconSun = document.getElementById("themeIconSun"); const themeIconMoon = document.getElementById("themeIconMoon"); /* ============================================================ * GET ROOM FROM QUERY-PARAMETER * ============================================================ * * Beispiel: * * https:///launch.html?room=abc-defg-hij * * ergibt: * * abc-defg-hij * * ============================================================ */ function getRoom() { const params = new URLSearchParams( window.location.search ); /* * Parameter nicht vorhanden. */ if (!params.has("room")) { return null; } /* * Leerzeichen etc. verhindern. * * "?room=" ohne Wert ergibt einen leeren String. */ return params.get("room").trim(); } /* ============================================================ * VALIDATE ROOM * ============================================================ * * Jitsi-Roomnamen können je nach Konfiguration relativ * unterschiedliche Zeichen enthalten. * * Wir verhindern hier lediglich Steuerzeichen und * offensichtliche URL-Manipulation. * * ============================================================ */ const ROOM_PATTERN = /^[A-Za-z0-9-]+$/; function isValidRoom(room) { if (!room) { return false; } /* * Nur Buchstaben, Zahlen und Bindestriche. */ return ROOM_PATTERN.test(room); } /* ============================================================ * JITSI WEB URL * ============================================================ */ function createBrowserUrl(room) { return ( "https://" + CONFIG.jitsiHost + "/" + encodeURIComponent(room) ); } /* ============================================================ * JITSI DESKTOP PROTOCOL URL * ============================================================ */ function createAppUrl(room) { return ( "jitsi-meet://" + CONFIG.jitsiHost + "/" + encodeURIComponent(room) ); } /* ============================================================ * JITSI ANDROID URL * ============================================================ */ /* * Die Android-App registriert "org.jitsi.meet", nicht das * Desktop-Schema. Chrome & Co. verlangen intent://, ohne App * bietet Chrome den Play-Store-Eintrag aus "package=" an. * Firefox ignoriert intent://, kennt aber das Custom-Schema. */ function createAndroidIntentUrl(room) { return ( "intent://" + CONFIG.jitsiHost + "/" + encodeURIComponent(room) + "#Intent;scheme=org.jitsi.meet;package=org.jitsi.meet;end" ); } const ANDROID_APP_SCHEME = "org.jitsi.meet://"; function createAndroidAppUrl(room) { return ( ANDROID_APP_SCHEME + CONFIG.jitsiHost + "/" + encodeURIComponent(room) ); } /* ============================================================ * URL NACH PLATTFORM/BROWSER * ============================================================ */ function selectProtocolUrl(room) { if (detectOperatingSystem() !== "android") { return createAppUrl(room); } return detectBrowser() === "firefox" ? createAndroidAppUrl(room) : createAndroidIntentUrl(room); } /* ============================================================ * DETECT OPERATING SYSTEM * ============================================================ */ /* * Einmal parsen (assets/ua-parser.min.js). * Fällt die Bibliothek aus, bleibt die Seite bedienbar. */ const uaResult = typeof UAParser === "function" ? new UAParser().getResult() : { os: {}, browser: {} }; /* * UAParser-Namen -> interne Kürzel. * iPadOS meldet sich als macOS – Touchpunkte gelten als iOS. */ const OS_NAMES = { Android: "android", iOS: "ios", Windows: "windows", macOS: "macos", Linux: "linux", Ubuntu: "linux", Debian: "linux", Fedora: "linux", Mint: "linux", CentOS: "linux", openSUSE: "linux", Raspbian: "linux" }; const BROWSER_NAMES = { Edge: "edge", "Mobile Edge": "edge", Firefox: "firefox", "Mobile Firefox": "firefox", Chrome: "chrome", "Mobile Chrome": "chrome", "Chrome Headless": "chrome", "Chrome WebView": "chrome", "Samsung Internet": "chrome", Safari: "safari", "Mobile Safari": "safari" }; function detectOperatingSystem() { /* * Chrome im "Desktop-Modus" auf Android sendet einen * Desktop-UA (z.B. X11; Linux). Client Hints verraten * das echte System. Firefox/Safari kennen sie nicht. */ const hints = navigator.userAgentData; if (hints && hints.platform === "Android") { return OS_NAMES.Android; } /* * iPadOS tarnt sich als macOS. */ if ( uaResult.os.name === "macOS" && navigator.maxTouchPoints > 1 ) { return OS_NAMES.iOS; } return OS_NAMES[uaResult.os.name] || "unknown"; } /* ============================================================ * BROWSER ERKENNEN * ============================================================ */ function detectBrowser() { return BROWSER_NAMES[uaResult.browser.name] || "unknown"; } /* ============================================================ * DOWNLOADS * ============================================================ */ function configureDownloads() { const os = detectOperatingSystem(); windowsDownload.classList.add("hidden"); macosDownload.classList.add("hidden"); linuxDownload.classList.add("hidden"); otherDownload.classList.add("hidden"); /* Windows */ if (os === "windows") { windowsLink.href = CONFIG.downloads.windows; windowsDownload.classList.remove("hidden"); return; } /* macOS */ if (os === "macos") { macosLink.href = CONFIG.downloads.macos; macosDownload.classList.remove("hidden"); return; } /* Linux */ if (os === "linux") { linuxLink.href = CONFIG.downloads.linux; linuxDownload.classList.remove("hidden"); return; } /* Alles andere */ otherDownload.classList.remove("hidden"); } /* ============================================================ * FALLBACK ANZEIGEN * ============================================================ */ function showFallback() { starting.classList.add("hidden"); launcher.classList.add("hidden"); fallback.classList.remove("hidden"); configureDownloads(); } /* ============================================================ * JITSI DESKTOP STARTEN * ============================================================ */ function launchJitsi() { const room = getRoom(); if (!isValidRoom(room)) { return; } const protocolUrl = selectProtocolUrl(room); const browserUrl = createBrowserUrl(room); /* * Browser-Fallback vorbereiten. */ browserLink.href = browserUrl; /* * UI auf "Startvorgang" setzen. */ launcher.classList.add("hidden"); fallback.classList.add("hidden"); starting.classList.remove("hidden"); let completed = false; let timeout = null; /* * Unsichtbarer iframe für den Protocol Handler. */ const iframe = document.createElement("iframe"); iframe.style.display = "none"; /* * Aufräumen. */ function cleanup() { if (completed) { return; } completed = true; if (timeout !== null) { clearTimeout(timeout); } iframe.remove(); } /* * -------------------------------------------------------- * WICHTIG: * * Kein "blur" mehr als Erfolgskriterium verwenden. * * Der Browser kann beim Protocol-Dialog ebenfalls * blur auslösen. * * Wenn der Benutzer den Dialog abbricht, bleibt die * Seite sichtbar und unser Timeout läuft weiter. * -------------------------------------------------------- */ document.body.appendChild(iframe); /* * Protocol Handler starten. * * Firefox ignoriert externe Protokolle aus iframes – * das Custom-Schema direkt in der Hauptnavigation starten. */ try { if (protocolUrl.startsWith(ANDROID_APP_SCHEME)) { window.location.href = protocolUrl; } else { iframe.src = protocolUrl; } } catch (error) { console.error( "Error opening the Jitsi Protocol Handler:", error ); cleanup(); showFallback(); return; } /* * -------------------------------------------------------- * FALLBACK * -------------------------------------------------------- * * Wir warten unabhängig davon, ob der Browser kurz * den Fokus verliert. * * Wenn Jitsi erfolgreich startet: * * -> Jitsi kommt in den Vordergrund * -> Benutzer sieht diese Seite nicht mehr * * Wenn der Benutzer den Browser-Dialog abbricht: * * -> diese Seite bleibt sichtbar * -> Timeout läuft ab * -> Fallback wird angezeigt * -------------------------------------------------------- */ timeout = setTimeout(() => { if (completed) { return; } cleanup(); showFallback(); }, CONFIG.launchTimeout); } /* ============================================================ * ROOM NAME * ============================================================ */ /* * Sonderzeichen in das nächstliegende ASCII-Zeichen * umwandeln. * * Beispiel: * * "Grundstück" ergibt "Grundstueck" * "François" ergibt "Francois" */ function transliterate(text) { const extras = { "ß": "ss", "ẞ": "ss", "æ": "ae", "Æ": "ae", "œ": "oe", "Œ": "oe", "ø": "o", "Ø": "o" }; return text .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[ßẞæÆœŒøØ]/g, (char) => extras[char]); } /* * Aus dem Meeting-Namen einen gültigen Room-Namen ableiten. * * Beispiel: * * "Grund Stück Planung" ergibt "grund-stuck-planung" */ function createRoomName(meetingName) { return transliterate(meetingName) .toLowerCase() .replace(/\s+/g, "-") .replace(/[^a-z0-9-]/g, ""); } /* ============================================================ * ICS FILE GENERATION * ============================================================ */ function generateICS(meetingName, description, startTime, endTime, organizer) { /* Generate a unique room name from the meeting name */ const roomName = createRoomName(meetingName); const meetingUrl = createLaunchUrl(roomName); /* Format dates for ICS */ const formatICSDate = (date) => { const pad = (num) => num.toString().padStart(2, '0'); const year = date.getFullYear(); const month = pad(date.getMonth() + 1); const day = pad(date.getDate()); const hours = pad(date.getHours()); const minutes = pad(date.getMinutes()); const seconds = pad(date.getSeconds()); return `${year}${month}${day}T${hours}${minutes}${seconds}`; }; const uid = `jitsi-${Date.now()}@${CONFIG.jitsiHost}`; const dtStamp = formatICSDate(new Date()); const dtStart = formatICSDate(startTime); const dtEnd = formatICSDate(endTime); /* Build ICS content */ const icsContent = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//BNVB//Jitsi Meeting//EN CALSCALE:GREGORIAN METHOD:PUBLISH BEGIN:VEVENT UID:${uid} DTSTAMP:${dtStamp} DTSTART:${dtStart} DTEND:${dtEnd} SUMMARY:${meetingName.replace(/,/g, '\\,')} DESCRIPTION:${description.replace(/,/g, '\\,').replace(/\n/g, '\\n')} LOCATION:${meetingUrl} ORGANIZER:MAILTO:${organizer} STATUS:CONFIRMED URL:${meetingUrl} SEQUENCE:0 TRANSP:OPAQUE BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Reminder TRIGGER:-PT15M END:VALARM END:VEVENT END:VCALENDAR`; return { content: icsContent, roomName: roomName, url: meetingUrl }; } function downloadICS(content, filename) { const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' }); const url = URL.createObjectURL(blob); return url; } const MINUTES_STEP = 15; const DEFAULT_DURATION_MINUTES = 30; /* * Minuten auf das 15-Minuten-Raster aufrunden. * * Beispiel: * * 14:07 ergibt 14:15 * 14:50 ergibt 15:00 */ function roundMinutes(date) { const rounded = new Date(date); rounded.setSeconds(0, 0); rounded.setMinutes( Math.ceil(date.getMinutes() / MINUTES_STEP) * MINUTES_STEP ); return rounded; } /* * Lokale Zeit für datetime-local formatieren. * * Beispiel: * * 1. Januar 2026, 14:15 ergibt "2026-01-01T14:15" */ function formatLocal(date) { const pad = (num) => num.toString().padStart(2, "0"); return ( date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + "T" + pad(date.getHours()) + ":" + pad(date.getMinutes()) ); } function showMeetingCreator() { /* Hide all other views */ launcher.classList.add("hidden"); starting.classList.add("hidden"); fallback.classList.add("hidden"); invalidRoom.classList.add("hidden"); /* Show meeting creator */ meetingCreator.classList.remove("hidden"); /* Reset form */ meetingNameInput.value = ''; meetingDescriptionInput.value = ''; meetingStartInput.value = ''; meetingEndInput.value = ''; meetingOrganizerInput.value = ''; icsSuccess.classList.add("hidden"); adhocResult.classList.add("hidden"); /* Set default start and end times */ const start = roundMinutes(new Date()); const end = new Date( start.getTime() + DEFAULT_DURATION_MINUTES * 60 * 1000 ); meetingStartInput.value = formatLocal(start); meetingEndInput.value = formatLocal(end); } function createMeeting() { const meetingName = meetingNameInput.value.trim(); const description = meetingDescriptionInput.value.trim(); const startTime = new Date(meetingStartInput.value); const endTime = new Date(meetingEndInput.value); const organizer = meetingOrganizerInput.value.trim(); if (!meetingName) { alert(getTranslation('creator.meetingNameRequired') || 'Please enter a meeting name'); return; } if (!ROOM_PATTERN.test(createRoomName(meetingName))) { alert(getTranslation('creator.meetingNameInvalid') || 'Meeting name may only contain letters, numbers and hyphens'); return; } if (!meetingStartInput.value) { alert(getTranslation('creator.startTimeRequired') || 'Please select a start time'); return; } if (!meetingEndInput.value) { alert(getTranslation('creator.endTimeRequired') || 'Please select an end time'); return; } if (startTime >= endTime) { alert(getTranslation('creator.endAfterStart') || 'End time must be after start time'); return; } if (!organizer) { alert(getTranslation('creator.organizerRequired') || 'Please enter an organizer email'); return; } const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(organizer)) { alert(getTranslation('creator.organizerInvalid') || 'Please enter a valid organizer email address'); return; } /* Generate ICS */ const icsData = generateICS(meetingName, description || meetingName, startTime, endTime, organizer); /* Create download link */ const blob = new Blob([icsData.content], { type: 'text/calendar;charset=utf-8' }); const url = URL.createObjectURL(blob); downloadIcsLink.href = url; downloadIcsLink.download = `${icsData.roomName}.ics`; /* Show success message */ icsSuccess.classList.remove("hidden"); adhocResult.classList.add("hidden"); /* Scroll to success message */ icsSuccess.scrollIntoView({ behavior: 'smooth' }); } /* ============================================================ * ADHOC LINK * ============================================================ */ /* * Zeitspanne, nach der der "Kopiert!"-Text zum * normalen Button-Label zurückkehrt. */ const COPY_RESET_MS = 2000; /* * Launch-URL, die das Meeting im Jitsi-Client öffnet. * * Beispiel: * * "projekt-kickoff" ergibt * https://meet.bnvb.eu/launch.html?room=projekt-kickoff */ function createLaunchUrl(roomName) { return `https://${CONFIG.jitsiLauncher}/launch.html?room=${encodeURIComponent(roomName)}`; } function createAdhocLink() { const meetingName = meetingNameInput.value.trim(); if (!meetingName) { alert(getTranslation('creator.meetingNameRequired') || 'Please enter a meeting name'); return; } const roomName = createRoomName(meetingName); if (!ROOM_PATTERN.test(roomName)) { alert(getTranslation('creator.meetingNameInvalid') || 'Meeting name may only contain letters, numbers and hyphens'); return; } const url = createLaunchUrl(roomName); adhocLink.href = url; adhocLink.textContent = url; icsSuccess.classList.add("hidden"); adhocResult.classList.remove("hidden"); adhocResult.scrollIntoView({ behavior: 'smooth' }); } async function copyAdhocLink() { const url = adhocLink.href; /* Clipboard-API, mit Fallback für ältere Browser */ try { await navigator.clipboard.writeText(url); } catch (error) { const input = document.createElement('textarea'); input.value = url; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); } /* Kurzes Feedback, dann zurück zum normalen Label */ copyAdhocLinkButton.textContent = getTranslation('creator.copiedLabel') || 'Copied!'; setTimeout(() => { copyAdhocLinkButton.textContent = getTranslation('creator.copyLinkButton') || 'Copy Link'; }, COPY_RESET_MS); } function cancelCreateMeeting() { meetingCreator.classList.add("hidden"); /* If there was a room parameter, show the launcher */ const room = getRoom(); if (isValidRoom(room)) { roomName.textContent = room; browserLink.href = createBrowserUrl(room); launcher.classList.remove("hidden"); } else { invalidRoom.classList.remove("hidden"); } } /* ============================================================ * INITIALISIERUNG * ============================================================ */ function initialize() { const room = getRoom(); /* * Kein room-Parameter: Meldungen ausblenden, * nur den Create-Button anzeigen. */ if (room === null) { missingRoomTitle.classList.add("hidden"); missingRoomError.classList.add("hidden"); invalidNameTitle.classList.add("hidden"); invalidNameError.classList.add("hidden"); invalidRoom.classList.remove("hidden"); createNewMeetingButton.classList.remove("hidden"); return; } /* * Leerer room-Parameter ("?room="): * "No meeting specified" anzeigen. */ if (room === "") { missingRoomTitle.classList.remove("hidden"); missingRoomError.classList.remove("hidden"); invalidNameTitle.classList.add("hidden"); invalidNameError.classList.add("hidden"); invalidRoom.classList.remove("hidden"); createNewMeetingButton.classList.remove("hidden"); return; } /* * Ungültiger room-Parameter: nur die * Invalid-Name-Meldung anzeigen. */ if (!isValidRoom(room)) { missingRoomTitle.classList.add("hidden"); missingRoomError.classList.add("hidden"); invalidNameTitle.classList.remove("hidden"); invalidNameError.classList.remove("hidden"); invalidRoomName.textContent = room; invalidRoom.classList.remove("hidden"); createNewMeetingButton.classList.remove("hidden"); return; } /* * Room anzeigen. */ roomName.textContent = room; /* * Browser-Fallback vorbereiten. */ browserLink.href = createBrowserUrl(room); /* * Launcher anzeigen. */ launcher.classList.remove("hidden"); /* * Hide the "Create New Meeting" button when a valid room is specified */ createNewFromLauncherButton.classList.add("hidden"); /* * Debug-Informationen. */ console.log( "Jitsi Launcher", { language: currentLanguage, room: room, operatingSystem: detectOperatingSystem(), browser: detectBrowser(), protocol: selectProtocolUrl(room), browserUrl: createBrowserUrl(room) } ); } /* ============================================================ * EVENTS * ============================================================ */ openButton.addEventListener( "click", launchJitsi ); retryButton.addEventListener( "click", launchJitsi ); /* Meeting Creator Events */ createNewMeetingButton.addEventListener( "click", showMeetingCreator ); createNewFromLauncherButton.addEventListener( "click", showMeetingCreator ); createMeetingButton.addEventListener( "click", createMeeting ); createAdhocButton.addEventListener( "click", createAdhocLink ); copyAdhocLinkButton.addEventListener( "click", copyAdhocLink ); cancelCreateButton.addEventListener( "click", cancelCreateMeeting ); /* Language menu events */ langButton.addEventListener( "click", toggleLanguageMenu ); langDropdown.addEventListener( "click", (event) => { const item = event.target.closest("button[data-lang]"); if (!item) { return; } setLanguage(item.dataset.lang); closeLanguageMenu(); } ); document.addEventListener( "click", (event) => { if (!langMenu.contains(event.target)) { closeLanguageMenu(); } if (!navbar.contains(event.target)) { closeNavMenu(); } } ); /* Theme events */ themeButton.addEventListener( "click", toggleTheme ); /* Hamburger events */ navToggle.addEventListener( "click", toggleNavMenu ); /* * Ohne gespeicherte Wahl dem System folgen. */ window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", () => { if (savedTheme() === null) { currentTheme = systemIsDark() ? "dark" : "light"; applyTheme(); } }); /* ============================================================ * START * ============================================================ */ async function loadTranslations() { try { const response = await fetch(I18N_URL); TRANSLATIONS = await response.json(); } catch (error) { /* * Ohne Datei bleibt der englische Text aus dem HTML stehen. */ console.error("Error loading translations:", error); } currentLanguage = detectLanguage(); translation = TRANSLATIONS[currentLanguage] || {}; } /* * Theme/Logo sofort anwenden, damit nichts aufblinkt – * Text erst nach dem Laden der Übersetzungen. */ async function start() { applyTheme(); applyBackgroundImage(); applyLogo(); await loadTranslations(); applyTranslations(); updateLanguageMenu(); initialize(); } start();