2413 lines
72 KiB
HTML
2413 lines
72 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;
|
||
align-items: center;
|
||
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;
|
||
flex: 0 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.logo-bar img {
|
||
display: block;
|
||
height: 60px;
|
||
max-width: 100%;
|
||
width: auto;
|
||
}
|
||
|
||
.nav-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* Hamburger: nur im Phone-Portrait sichtbar.
|
||
* "button.nav-toggle" ist spezifischer als ".button",
|
||
* sonst überschreibt display: inline-block diese Regel. */
|
||
button.nav-toggle {
|
||
display: none;
|
||
}
|
||
|
||
/* Enger als der globale 6px-Margin der Buttons */
|
||
.navbar .button {
|
||
margin: 0;
|
||
}
|
||
|
||
.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, 100%);
|
||
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;
|
||
}
|
||
|
||
.adhoc-result {
|
||
margin-top: 24px;
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
background: #f0fff4;
|
||
color: #1a7f37;
|
||
}
|
||
|
||
.adhoc-link {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.adhoc-link a {
|
||
font-family: monospace;
|
||
word-break: break-all;
|
||
color: #0066cc;
|
||
}
|
||
|
||
/*
|
||
* Verhindert abgeschnittene Inhalte, wenn die Karte
|
||
* höher als der Viewport ist ("center" schneidet oben ab).
|
||
*/
|
||
@supports (align-items: safe center) {
|
||
body {
|
||
align-items: safe center;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Schmale Bildschirme: Navbar umbrechen lassen,
|
||
* Abstände und Logo verkleinern.
|
||
*/
|
||
@media (max-width: 600px) {
|
||
body {
|
||
/* 130px räumen die fixe Navbar frei */
|
||
padding-top: 130px;
|
||
/* min-height gilt für die Content-Box: Navbar-Freiraum
|
||
* abziehen, sonst sitzt die Karte unter der Mitte */
|
||
min-height: calc(100vh - 130px);
|
||
}
|
||
|
||
.navbar {
|
||
top: 8px;
|
||
left: 4%;
|
||
width: 92%;
|
||
padding: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.logo-bar img {
|
||
height: 40px;
|
||
}
|
||
|
||
button, .button {
|
||
padding: 10px 14px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.card {
|
||
padding: 20px;
|
||
}
|
||
}
|
||
|
||
/* Sehr schmale Telefone: noch kompakter */
|
||
@media (max-width: 400px) {
|
||
.navbar {
|
||
left: 2%;
|
||
width: 96%;
|
||
padding: 8px;
|
||
gap: 6px;
|
||
}
|
||
|
||
.logo-bar img {
|
||
height: 32px;
|
||
}
|
||
|
||
.nav-actions {
|
||
gap: 6px;
|
||
}
|
||
|
||
.nav-actions .button {
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Phone-Portrait: Navbar-Aktionen hinter dem
|
||
* Hamburger-Button als Dropdown anzeigen.
|
||
*/
|
||
@media (orientation: portrait) and (max-width: 600px) {
|
||
body {
|
||
/* Navbar ist hier einzeilig */
|
||
padding-top: 80px;
|
||
min-height: calc(100vh - 80px);
|
||
}
|
||
|
||
button.nav-toggle {
|
||
display: inline-flex;
|
||
}
|
||
|
||
/* ".navbar .button" setzt margin: 0 – deshalb
|
||
* höher spezifisch ans Ziel: Button nach rechts */
|
||
.navbar .nav-toggle {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.nav-actions {
|
||
display: none;
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
margin: 8px 0 0;
|
||
padding: 8px;
|
||
min-width: 200px;
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.nav-actions.open {
|
||
display: flex;
|
||
}
|
||
|
||
/* Alle Panel-Buttons gleich breit, Text zentriert */
|
||
.nav-actions .button {
|
||
text-align: center;
|
||
}
|
||
|
||
/* Sprach-Button liegt im .lang-menu-Wrapper */
|
||
.nav-actions .lang-menu .button {
|
||
display: block;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root.dark .nav-actions {
|
||
background: #242424;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Sicherheit: Hamburger im Landscape grundsätzlich
|
||
* ausblenden, falls orientation falsch gemeldet wird.
|
||
*/
|
||
@media (orientation: landscape) {
|
||
button.nav-toggle {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Phone-Landscape: schmaler, hoher Karteninhalt –
|
||
* einzeilige Navbar (~106px) freihalten.
|
||
*/
|
||
@media (orientation: landscape) and (max-height: 500px) {
|
||
body {
|
||
padding-top: 120px;
|
||
/* min-height gilt für die Content-Box: Navbar-Freiraum
|
||
* abziehen, sonst sitzt die Karte unter der Mitte */
|
||
min-height: calc(100vh - 120px);
|
||
}
|
||
}
|
||
|
||
@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 .navbar {
|
||
background: rgba(36, 36, 36, 0.72);
|
||
}
|
||
|
||
: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;
|
||
}
|
||
|
||
:root.dark .adhoc-result {
|
||
background: #1c3527;
|
||
color: #7ee2a8;
|
||
}
|
||
|
||
:root.dark .logo-bar img {
|
||
filter: url(#logo-dark-tint);
|
||
}
|
||
|
||
:root.dark .adhoc-link a {
|
||
color: #99ccff;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<svg width="0" height="0" style="position: absolute;" aria-hidden="true">
|
||
<filter id="logo-dark-tint">
|
||
<feColorMatrix type="matrix" values="0 0 0 0 0.8667
|
||
0 0 0 0 0.8431
|
||
0 0 0 0 0.8235
|
||
0 0 0 1 0"/>
|
||
</filter>
|
||
</svg>
|
||
|
||
<nav class="navbar">
|
||
<div id="logoBar" class="logo-bar hidden">
|
||
<img id="logoImage" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Logo">
|
||
</div>
|
||
<button id="navToggle" class="button secondary nav-toggle" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="navActions" aria-label="Menu">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 7h16M4 12h16M4 17h16"/>
|
||
</svg>
|
||
</button>
|
||
<div id="navActions" class="nav-actions">
|
||
<a class="button secondary" href="https://www.bnvb-ingenieure.de/impressum" target="_blank" rel="noopener noreferrer" data-i18n="impressum">
|
||
Impressum
|
||
</a>
|
||
<a class="button secondary" href="https://git.bnvb.eu/stefan.tollkuehn/jitsi-launch" target="_blank" rel="noopener noreferrer" data-i18n="source">
|
||
Source
|
||
</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="createAdhocButton" class="button secondary" type="button" data-i18n="creator.adhocButton">
|
||
Create Adhoc Link
|
||
</button>
|
||
<button id="cancelCreateButton" class="button secondary" type="button" data-i18n="creator.cancelButton">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
|
||
<div id="adhocResult" class="adhoc-result hidden">
|
||
<span data-i18n="creator.adhocLinkLabel">Your adhoc meeting link:</span>
|
||
<div class="adhoc-link">
|
||
<a id="adhocLink" target="_blank" rel="noopener noreferrer"></a>
|
||
<button id="copyAdhocLinkButton" class="button secondary" type="button" data-i18n="creator.copyLinkButton">Copy Link</button>
|
||
</div>
|
||
</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 src="assets/ua-parser.min.js"></script>
|
||
<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",
|
||
source: "Source",
|
||
|
||
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",
|
||
adhocButton: "Create Adhoc Link",
|
||
adhocLinkLabel: "Your adhoc meeting link:",
|
||
copyLinkButton: "Copy Link",
|
||
copiedLabel: "Copied!",
|
||
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",
|
||
source: "Quellcode",
|
||
|
||
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",
|
||
adhocButton: "Adhoc-Link erstellen",
|
||
adhocLinkLabel: "Ihr Adhoc-Meeting-Link:",
|
||
copyLinkButton: "Link kopieren",
|
||
copiedLabel: "Kopiert!",
|
||
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",
|
||
source: "Code source",
|
||
|
||
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",
|
||
adhocButton: "Créer un lien adhoc",
|
||
adhocLinkLabel: "Votre lien de réunion adhoc :",
|
||
copyLinkButton: "Copier le lien",
|
||
copiedLabel: "Copié !",
|
||
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: "法律声明",
|
||
source: "源代码",
|
||
|
||
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: "创建新会议",
|
||
adhocButton: "创建临时链接",
|
||
adhocLinkLabel: "您的临时会议链接:",
|
||
copyLinkButton: "复制链接",
|
||
copiedLabel: "已复制!",
|
||
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");
|
||
}
|
||
|
||
/* ============================================================
|
||
* 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://<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
|
||
* ============================================================
|
||
*/
|
||
|
||
/*
|
||
* 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() {
|
||
/*
|
||
* 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 = 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 = 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: 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
|
||
);
|
||
|
||
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
|
||
* ============================================================
|
||
*/
|
||
|
||
applyTranslations();
|
||
applyTheme();
|
||
applyBackgroundImage();
|
||
applyLogo();
|
||
updateLanguageMenu();
|
||
initialize();
|
||
</script>
|
||
</body>
|
||
</html> |