Files
jitsi-launch/launch.html
T

2048 lines
61 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jitsi Meeting</title>
<link rel='shortlink' href='https://meet.bnvb.eu/' />
<link href="https://meet.bnvb.eu/assets/favicon.ico" rel="alternate icon">
<link href="https://meet.bnvb.eu/assets/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
<link href="https://meet.bnvb.eu/assets/favicon.svg" rel="icon" type="image/svg+xml">
<style>
:root {
color-scheme: light;
}
:root.dark {
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f5;
color: #222;
}
.container {
display: flex;
flex-direction: column;
width: min(700px, calc(100% - 32px));
}
.navbar {
position: fixed;
top: 16px;
left: 2%;
width: 96%;
box-sizing: border-box;
z-index: 100;
display: flex;
align-items: center;
gap: 8px;
padding: 15px;
background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(7px);
backdrop-filter: blur(7px);
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.30), 0 12px 40px rgba(0, 0, 0, 0.45);
}
.logo-bar {
display: flex;
align-items: center;
}
.logo-bar img {
display: block;
height: 60px;
width: auto;
}
.nav-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.lang-menu {
position: relative;
}
.lang-dropdown {
display: none;
position: absolute;
right: 0;
top: 100%;
min-width: 150px;
margin-top: 6px;
padding: 6px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.lang-dropdown.open {
display: block;
}
.lang-dropdown button {
display: block;
width: 100%;
margin: 0;
padding: 10px 14px;
border: 0;
border-radius: 6px;
background: none;
text-align: left;
font-size: 14px;
font-family: inherit;
cursor: pointer;
}
.lang-dropdown button:hover {
background: #f0f0f0;
}
.lang-dropdown button.active {
font-weight: 600;
background: #f0f8ff;
}
.lang-flag {
width: 20px;
height: 13px;
margin-right: 6px;
border-radius: 2px;
vertical-align: -2px;
}
.card {
box-sizing: border-box;
width: min(600px, calc(100% - 32px));
padding: 32px;
background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(7px);
backdrop-filter: blur(7px);
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.30), 0 12px 40px rgba(0, 0, 0, 0.45);
text-align: center;
}
h1 {
margin-top: 0;
}
p {
color: #555;
}
button, .button {
display: inline-block;
border: 0;
border-radius: 8px;
padding: 12px 20px;
margin: 6px;
font-size: 16px;
font-family: inherit;
cursor: pointer;
text-decoration: none;
}
.primary {
background: #1f73e8;
color: white;
}
.secondary {
background: #e9e9e9;
color: #222;
}
.hidden {
display: none !important;
}
.status {
margin-top: 24px;
padding: 16px;
border-radius: 8px;
background: #f0f0f0;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: -3px;
}
.download {
margin-top: 24px;
padding: 20px;
border-radius: 8px;
background: #f5f5f5;
}
.room {
font-family: monospace;
word-break: break-all;
}
.error {
padding: 16px;
border-radius: 8px;
background: #fff0f0;
color: #900;
}
.form-group {
margin-bottom: 16px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-actions {
margin-top: 24px;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.success-message {
margin-top: 24px;
padding: 16px;
border-radius: 8px;
background: #f0f8ff;
color: #0066cc;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/*
* Dark-Theme über die Klasse "dark" am html-Element.
* JS setzt sie anhand der gespeicherten Wahl oder der
* System-Einstellung (siehe detectTheme).
*/
:root.dark body {
background: #181818;
color: #eee;
}
:root.dark .card {
background: rgba(36, 36, 36, 0.72);
}
:root.dark .lang-dropdown {
background: #242424;
}
:root.dark .lang-dropdown button:hover {
background: #303030;
}
:root.dark .lang-dropdown button.active {
background: #1a3a5a;
}
:root.dark p {
color: #bbb;
}
:root.dark .secondary {
background: #444;
color: #eee;
}
:root.dark .status,
:root.dark .download {
background: #303030;
}
:root.dark .spinner {
border-color: #555;
border-top-color: #ddd;
}
:root.dark .form-group label {
color: #ddd;
}
:root.dark .form-group input,
:root.dark .form-group textarea {
background: #303030;
border-color: #555;
color: #eee;
}
:root.dark .success-message {
background: #1a3a5a;
color: #99ccff;
}
</style>
</head>
<body>
<nav class="navbar">
<div id="logoBar" class="logo-bar hidden">
<img id="logoImage" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Logo">
</div>
<div class="nav-actions">
<a class="button secondary" href="https://www.bnvb-ingenieure.de/impressum" target="_blank" rel="noopener noreferrer" data-i18n="impressum">
Impressum
</a>
<div id="langMenu" class="lang-menu">
<button id="langButton" class="button secondary" type="button" aria-haspopup="true" aria-expanded="false">
<img id="langFlag" class="lang-flag" src="assets/uk.svg" alt="">
<span id="langLabel">English</span>
</button>
<div id="langDropdown" class="lang-dropdown">
<button type="button" data-lang="en"><img class="lang-flag" src="assets/uk.svg" alt="">English</button>
<button type="button" data-lang="de"><img class="lang-flag" src="assets/de.svg" alt="">Deutsch</button>
<button type="button" data-lang="fr"><img class="lang-flag" src="assets/fr.svg" alt="">Français</button>
<button type="button" data-lang="zh"><img class="lang-flag" src="assets/cn.svg" alt="">中文</button>
</div>
</div>
<button id="themeButton" class="button secondary" type="button" aria-label="Toggle dark mode">
<svg id="themeIconMoon" class="theme-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
</svg>
<svg id="themeIconSun" class="theme-icon hidden" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>
</svg>
</button>
</div>
</nav>
<div class="container">
<div class="card">
<!-- NORMALER LAUNCHER -->
<div id="launcher" class="hidden">
<h1 data-i18n="launcher.title">Jitsi Meeting</h1>
<p>
<span data-i18n="launcher.meetingPrefix">
The meeting
</span>
<span class="room">
<strong id="roomName"></strong>
</span>
<span data-i18n="launcher.meetingSuffix">
will be opened.
</span>
</p>
<button id="openButton" class="button primary" type="button" data-i18n="launcher.openButton">
Open meeting
</button>
<button id="createNewFromLauncherButton" class="button secondary hidden" type="button" data-i18n="creator.createNewButton">
Create New Meeting
</button>
</div>
<!-- STARTVORGANG -->
<div id="starting" class="hidden">
<h1 data-i18n="starting.title">
Starting Jitsi Meet …
</h1>
<div class="status">
<span class="spinner"></span>
<span data-i18n="starting.status">
The Jitsi Meet desktop client is being opened.
</span>
</div>
</div>
<!-- FALLBACK -->
<div id="fallback" class="hidden">
<h1 data-i18n="fallback.title">
Jitsi Meet could not be opened
</h1>
<p data-i18n="fallback.description">
The Jitsi Meet desktop client may not have been started.
</p>
<button id="retryButton" class="button primary" type="button" data-i18n="fallback.retryButton">
Try again
</button>
<!-- Windows -->
<div id="windowsDownload" class="download hidden">
<h2 data-i18n="downloads.windows.title">
Jitsi Meet for Microsoft Windows
</h2>
<p data-i18n="downloads.windows.description">
Install the Jitsi Meet desktop client and then try opening the meeting again.
</p>
<a id="windowsLink" class="button secondary" target="_blank" rel="noopener noreferrer" data-i18n="downloads.windows.button">
Download Jitsi for Windows
</a>
</div>
<!-- macOS -->
<div id="macosDownload" class="download hidden">
<h2 data-i18n="downloads.macos.title">
Jitsi Meet for Apple macOS
</h2>
<p data-i18n="downloads.macos.description">
Install the Jitsi Meet desktop client and then try opening the meeting again.
</p>
<a id="macosLink" class="button secondary" target="_blank" rel="noopener noreferrer" data-i18n="downloads.macos.button">
Download Jitsi for macOS
</a>
</div>
<!-- Linux -->
<div id="linuxDownload" class="download hidden">
<h2 data-i18n="downloads.linux.title">
Jitsi Meet for Linux
</h2>
<p data-i18n="downloads.linux.description">
Install the Jitsi Meet desktop client and then try opening the meeting again.
</p>
<a id="linuxLink" class="button secondary" target="_blank" rel="noopener noreferrer" data-i18n="downloads.linux.button">
Download Jitsi for Linux
</a>
</div>
<!-- unknown -->
<div id="otherDownload" class="download hidden">
<h2 data-i18n="downloads.other.title">
Jitsi Meet Desktop
</h2>
<p data-i18n="downloads.other.description">
No desktop download is configured for your operating system.
</p>
</div>
<!-- Browser -->
<div class="download">
<h2 data-i18n="browser.title">
Join in browser
</h2>
<p data-i18n="browser.description">
You can also join the meeting directly in your browser.
</p>
<a id="browserLink" class="button secondary" data-i18n="browser.button">
Join in browser
</a>
</div>
</div>
<!-- FEHLER -->
<div id="invalidRoom" class="hidden">
<h1 id="missingRoomTitle" data-i18n="error.title">
No meeting specified
</h1>
<div id="missingRoomError" class="error">
<span data-i18n="error.description">
Please specify a meeting using the
</span>
<code>?room=...</code>
<span data-i18n="error.descriptionSuffix">
parameter.
</span>
</div>
<h1 id="invalidNameTitle" class="hidden" data-i18n="error.invalidTitle">
Invalid meeting name
</h1>
<div id="invalidNameError" class="error hidden">
<span data-i18n="error.invalidDescription">
Only letters, numbers and hyphens are allowed:
</span>
<br>
<span class="room">
<strong id="invalidRoomName"></strong>
</span>
</div>
<div style="margin-top: 24px;">
<button id="createNewMeetingButton" class="button primary hidden" type="button" data-i18n="creator.createNewButton">
Create New Meeting
</button>
</div>
</div>
<!-- MEETING CREATOR -->
<div id="meetingCreator" class="hidden">
<h1 data-i18n="creator.title">Create New Meeting</h1>
<div class="form-group">
<label for="meetingName" data-i18n="creator.meetingNameLabel">Meeting Name</label>
<input type="text" id="meetingName" placeholder="My Meeting" />
</div>
<div class="form-group">
<label for="meetingDescription" data-i18n="creator.meetingDescriptionLabel">Description</label>
<textarea id="meetingDescription" placeholder="Meeting description (optional)"></textarea>
</div>
<div class="form-group">
<label for="meetingStart" data-i18n="creator.meetingStartLabel">Start Date & Time</label>
<input type="datetime-local" id="meetingStart" step="900" />
</div>
<div class="form-group">
<label for="meetingEnd" data-i18n="creator.meetingEndLabel">End Date & Time</label>
<input type="datetime-local" id="meetingEnd" step="900" />
</div>
<div class="form-group">
<label for="meetingOrganizer" data-i18n="creator.meetingOrganizerLabel">Organizer Email</label>
<input type="email" id="meetingOrganizer" placeholder="organizer@example.com" />
</div>
<div class="form-actions">
<button id="createMeetingButton" class="button primary" type="button" data-i18n="creator.createButton">
Create Meeting
</button>
<button id="cancelCreateButton" class="button secondary" type="button" data-i18n="creator.cancelButton">
Cancel
</button>
</div>
<div id="icsSuccess" class="success-message hidden">
<span data-i18n="creator.successMessage">Meeting created! Download your calendar file and open it to import the meeting into your personal calendar.</span>
<a id="downloadIcsLink" class="button primary" style="margin-top: 12px; display: inline-block;" data-i18n="creator.downloadButton">Download ICS File</a>
</div>
</div>
</div>
</div>
<script>
"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
* ============================================================
*/
const TRANSLATIONS = {
en: {
pageTitle: "Jitsi Meeting",
impressum: "Legal Notice",
launcher: {
title: "Jitsi Meeting",
meetingPrefix: "The meeting",
meetingSuffix: "will be joined.",
openButton: "Join meeting"
},
starting: {
title: "Starting Jitsi Meet …",
status: "The Jitsi Meet desktop client is being opened."
},
fallback: {
title: "Jitsi Meet could not be opened",
description: "The Jitsi Meet desktop client may not have been started.",
retryButton: "Try again"
},
downloads: {
windows: {
title: "Jitsi Meet for Windows",
description:
"Install the Jitsi Meet desktop client and then try opening the meeting again.",
button: "Download Jitsi for Windows"
},
macos: {
title: "Jitsi Meet for macOS",
description:
"Install the Jitsi Meet desktop client and then try opening the meeting again.",
button: "Download Jitsi for macOS"
},
linux: {
title: "Jitsi Meet for Linux",
description:
"Install the Jitsi Meet desktop client and then try opening the meeting again.",
button: "Download Jitsi for Linux"
},
other: {
title: "Jitsi Meet Desktop",
description:
"No desktop download is configured for your operating system."
}
},
browser: {
title: "Join in browser",
description:
"You can also join the meeting directly in your browser.",
button: "Join in browser"
},
error: {
title: "No meeting specified",
description: "Please specify a meeting using the",
descriptionSuffix: "parameter.",
invalidTitle: "Invalid meeting name",
invalidDescription: "Only letters, numbers and hyphens are allowed:"
},
creator: {
title: "Create New Meeting",
meetingNameLabel: "Meeting Name",
meetingDescriptionLabel: "Description",
meetingStartLabel: "Start Date & Time",
meetingEndLabel: "End Date & Time",
meetingOrganizerLabel: "Organizer Email",
createButton: "Create Meeting",
cancelButton: "Cancel",
successMessage: "Meeting created! Download your calendar file and open it to import the meeting into your personal calendar.",
downloadButton: "Download ICS File",
createNewButton: "Create New Meeting",
meetingNameRequired: "Please enter a meeting name",
meetingNameInvalid: "Meeting name may only contain letters, numbers and hyphens",
startTimeRequired: "Please select a start time",
endTimeRequired: "Please select an end time",
endAfterStart: "End time must be after start time",
organizerRequired: "Please enter an organizer email",
organizerInvalid: "Please enter a valid organizer email address"
}
},
de: {
pageTitle: "Jitsi Meeting öffnen",
impressum: "Impressum",
launcher: {
title: "Jitsi Meeting",
meetingPrefix: "Das Meeting",
meetingSuffix: "wird geöffnet.",
openButton: "An Meeting teilnehmen"
},
starting: {
title: "Jitsi Meet wird gestartet …",
status: "Der Jitsi Meet Desktop-Client wird geöffnet."
},
fallback: {
title: "Jitsi Meet konnte nicht geöffnet werden",
description:
"Der Jitsi Meet Desktop-Client wurde möglicherweise nicht gestartet.",
retryButton: "Erneut versuchen"
},
downloads: {
windows: {
title: "Jitsi Meet für Windows",
description:
"Installieren Sie den Jitsi Meet Desktop-Client und versuchen Sie anschließend erneut, das Meeting zu öffnen.",
button: "Jitsi für Windows herunterladen"
},
macos: {
title: "Jitsi Meet für macOS",
description:
"Installieren Sie den Jitsi Meet Desktop-Client und versuchen Sie anschließend erneut, das Meeting zu öffnen.",
button: "Jitsi für macOS herunterladen"
},
linux: {
title: "Jitsi Meet für Linux",
description:
"Installieren Sie den Jitsi Meet Desktop-Client und versuchen Sie anschließend erneut, das Meeting zu öffnen.",
button: "Jitsi für Linux herunterladen"
},
other: {
title: "Jitsi Meet Desktop",
description:
"Für Ihr Betriebssystem ist kein Desktop-Download hinterlegt."
}
},
browser: {
title: "Im Browser teilnehmen",
description:
"Sie können dem Meeting auch direkt über Ihren Browser beitreten.",
button: "Im Browser beitreten"
},
error: {
title: "Kein Meeting angegeben",
description:
"Bitte geben Sie ein Meeting über den Parameter",
descriptionSuffix: "an.",
invalidTitle: "Ungültiger Meeting-Name",
invalidDescription: "Erlaubt sind nur Buchstaben, Zahlen und Bindestriche:"
},
creator: {
title: "Neues Meeting erstellen",
meetingNameLabel: "Meeting-Name",
meetingDescriptionLabel: "Beschreibung",
meetingStartLabel: "Start Datum & Uhrzeit",
meetingEndLabel: "Ende Datum & Uhrzeit",
meetingOrganizerLabel: "Organisator E-Mail",
createButton: "Meeting erstellen",
cancelButton: "Abbrechen",
successMessage: "Meeting erstellt! Laden Sie Ihre Kalenderdatei herunter und öffnen Sie diese, um das Meeting in Ihren persönlichen Kalender zu importieren.",
downloadButton: "ICS-Datei herunterladen",
createNewButton: "Neues Meeting erstellen",
meetingNameRequired: "Bitte geben Sie einen Meeting-Namen ein",
meetingNameInvalid: "Der Meeting-Name darf nur Buchstaben, Zahlen und Bindestriche enthalten",
startTimeRequired: "Bitte wählen Sie eine Startzeit",
endTimeRequired: "Bitte wählen Sie eine Endzeit",
endAfterStart: "Die Endzeit muss nach der Startzeit liegen",
organizerRequired: "Bitte geben Sie eine Organisator-E-Mail ein",
organizerInvalid: "Bitte geben Sie eine gültige Organisator-E-Mail-Adresse ein"
}
},
fr: {
pageTitle: "Réunion Jitsi",
impressum: "Mentions légales",
launcher: {
title: "Réunion Jitsi",
meetingPrefix: "La réunion",
meetingSuffix: "va être ouverte.",
openButton: "Rejoindre la réunion"
},
starting: {
title: "Démarrage de Jitsi Meet…",
status: "Le client bureau Jitsi Meet est en cours d'ouverture."
},
fallback: {
title: "Jitsi Meet n'a pas pu être ouvert",
description:
"Le client bureau Jitsi Meet n'a peut-être pas été démarré.",
retryButton: "Réessayer"
},
downloads: {
windows: {
title: "Jitsi Meet pour Windows",
description:
"Installez le client bureau Jitsi Meet, puis essayez de rouvrir la réunion.",
button: "Télécharger Jitsi pour Windows"
},
macos: {
title: "Jitsi Meet pour macOS",
description:
"Installez le client bureau Jitsi Meet, puis essayez de rouvrir la réunion.",
button: "Télécharger Jitsi pour macOS"
},
linux: {
title: "Jitsi Meet pour Linux",
description:
"Installez le client bureau Jitsi Meet, puis essayez de rouvrir la réunion.",
button: "Télécharger Jitsi pour Linux"
},
other: {
title: "Jitsi Meet Desktop",
description:
"Aucun téléchargement bureau n'est configuré pour votre système d'exploitation."
}
},
browser: {
title: "Rejoindre dans le navigateur",
description:
"Vous pouvez également rejoindre la réunion directement dans votre navigateur.",
button: "Rejoindre dans le navigateur"
},
error: {
title: "Aucune réunion spécifiée",
description: "Veuillez spécifier une réunion à l'aide du paramètre",
descriptionSuffix: ".",
invalidTitle: "Nom de réunion invalide",
invalidDescription: "Seuls les lettres, les chiffres et les tirets sont autorisés :"
},
creator: {
title: "Créer une nouvelle réunion",
meetingNameLabel: "Nom de la réunion",
meetingDescriptionLabel: "Description",
meetingStartLabel: "Date et heure de début",
meetingEndLabel: "Date et heure de fin",
meetingOrganizerLabel: "Email de l'organisateur",
createButton: "Créer la réunion",
cancelButton: "Annuler",
successMessage: "Réunion créée ! Téléchargez votre fichier calendrier et ouvrez-le pour importer la réunion dans votre calendrier personnel.",
downloadButton: "Télécharger le fichier ICS",
createNewButton: "Créer une nouvelle réunion",
meetingNameRequired: "Veuillez entrer un nom de réunion",
meetingNameInvalid: "Le nom de la réunion ne peut contenir que des lettres, des chiffres et des tirets",
startTimeRequired: "Veuillez sélectionner une heure de début",
endTimeRequired: "Veuillez sélectionner une heure de fin",
endAfterStart: "L'heure de fin doit être après l'heure de début",
organizerRequired: "Veuillez entrer un email d'organisateur",
organizerInvalid: "Veuillez entrer une adresse email d'organisateur valide"
}
},
zh: {
pageTitle: "Jitsi 会议",
impressum: "法律声明",
launcher: {
title: "Jitsi 会议",
meetingPrefix: "会议",
meetingSuffix: "即将打开。",
openButton: "加入会议"
},
starting: {
title: "正在启动 Jitsi Meet …",
status: "正在打开 Jitsi Meet 桌面客户端。"
},
fallback: {
title: "无法打开 Jitsi Meet",
description: "Jitsi Meet 桌面客户端可能尚未启动。",
retryButton: "重试"
},
downloads: {
windows: {
title: "适用于 Windows 的 Jitsi Meet",
description: "请安装 Jitsi Meet 桌面客户端,然后再次尝试打开会议。",
button: "下载 Windows 版 Jitsi"
},
macos: {
title: "适用于 macOS 的 Jitsi Meet",
description: "请安装 Jitsi Meet 桌面客户端,然后再次尝试打开会议。",
button: "下载 macOS 版 Jitsi"
},
linux: {
title: "适用于 Linux 的 Jitsi Meet",
description: "请安装 Jitsi Meet 桌面客户端,然后再次尝试打开会议。",
button: "下载 Linux 版 Jitsi"
},
other: {
title: "Jitsi Meet 桌面版",
description: "没有为您的操作系统配置桌面版下载。"
}
},
browser: {
title: "在浏览器中加入",
description: "您也可以直接通过浏览器加入会议。",
button: "在浏览器中加入"
},
error: {
title: "未指定会议",
description: "请使用参数指定会议",
descriptionSuffix: "。",
invalidTitle: "会议名称无效",
invalidDescription: "仅允许使用字母、数字和连字符:"
},
creator: {
title: "创建新会议",
meetingNameLabel: "会议名称",
meetingDescriptionLabel: "描述",
meetingStartLabel: "开始日期和时间",
meetingEndLabel: "结束日期和时间",
meetingOrganizerLabel: "组织者邮箱",
createButton: "创建会议",
cancelButton: "取消",
successMessage: "会议已创建!请下载您的日历文件并打开它,将会议导入您的个人日历。",
downloadButton: "下载 ICS 文件",
createNewButton: "创建新会议",
meetingNameRequired: "请输入会议名称",
meetingNameInvalid: "会议名称只能包含字母、数字和连字符",
startTimeRequired: "请选择开始时间",
endTimeRequired: "请选择结束时间",
endAfterStart: "结束时间必须晚于开始时间",
organizerRequired: "请输入组织者邮箱",
organizerInvalid: "请输入有效的组织者邮箱地址"
}
}
};
/* ============================================================
* 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 = detectLanguage();
let translation = TRANSLATIONS[currentLanguage];
/* ============================================================
* Apply translations to webpage
* ============================================================
*/
function applyTranslations() {
document.documentElement.lang = currentLanguage;
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");
}
/* ============================================================
* 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 cancelCreateButton = document.getElementById("cancelCreateButton");
const icsSuccess = document.getElementById("icsSuccess");
const downloadIcsLink = document.getElementById("downloadIcsLink");
/* 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://<YOURJITSIINSTANCEADDRESS>/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)
);
}
/* ============================================================
* DETECT OPERATING SYSTEM
* ============================================================
*/
function detectOperatingSystem() {
const ua = navigator.userAgent;
/* iPhone / iPad / iPod */
if (
/iPhone|iPad|iPod/i.test(ua) ||
(
navigator.platform === "MacIntel" &&
navigator.maxTouchPoints > 1
)
) {
return "ios";
}
/* Android */
if (/Android/i.test(ua)) {
return "android";
}
/* Windows */
if (/Windows/i.test(ua)) {
return "windows";
}
/* macOS */
if (/Macintosh|Mac OS X/i.test(ua)) {
return "macos";
}
/* Linux */
if (/Linux/i.test(ua)) {
return "linux";
}
return "unknown";
}
/* ============================================================
* BROWSER ERKENNEN
* ============================================================
*/
function detectBrowser() {
const ua = navigator.userAgent;
if (/Edg\//i.test(ua)) {
return "edge";
}
if (/Firefox\//i.test(ua)) {
return "firefox";
}
if (
/Chrome\//i.test(ua) &&
!/Edg\//i.test(ua)
) {
return "chrome";
}
if (
/Safari\//i.test(ua) &&
!/Chrome\//i.test(ua) &&
!/Chromium\//i.test(ua)
) {
return "safari";
}
return "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 = createAppUrl(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.
*/
try {
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 = `https://${CONFIG.jitsiLauncher}/launch.html?room=${encodeURIComponent(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");
/* 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");
/* Scroll to success message */
icsSuccess.scrollIntoView({ behavior: 'smooth' });
}
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: createAppUrl(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
);
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();
}
}
);
/* Theme events */
themeButton.addEventListener(
"click",
toggleTheme
);
/*
* Ohne gespeicherte Wahl dem System folgen.
*/
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
if (savedTheme() === null) {
currentTheme = systemIsDark() ? "dark" : "light";
applyTheme();
}
});
/* ============================================================
* START
* ============================================================
*/
applyTranslations();
applyTheme();
applyBackgroundImage();
applyLogo();
updateLanguageMenu();
initialize();
</script>
</body>
</html>