1711 lines
49 KiB
HTML
1711 lines
49 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>
|
|
|
|
<style>
|
|
:root {
|
|
color-scheme: light 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;
|
|
right: 16px;
|
|
z-index: 100;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.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: white;
|
|
border-radius: 14px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.10);
|
|
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);
|
|
}
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background: #181818;
|
|
color: #eee;
|
|
}
|
|
|
|
.card {
|
|
background: #242424;
|
|
}
|
|
|
|
.lang-dropdown {
|
|
background: #242424;
|
|
}
|
|
|
|
.lang-dropdown button:hover {
|
|
background: #303030;
|
|
}
|
|
|
|
.lang-dropdown button.active {
|
|
background: #1a3a5a;
|
|
}
|
|
|
|
p {
|
|
color: #bbb;
|
|
}
|
|
|
|
.secondary {
|
|
background: #444;
|
|
color: #eee;
|
|
}
|
|
|
|
.status, .download {
|
|
background: #303030;
|
|
}
|
|
|
|
.spinner {
|
|
border-color: #555;
|
|
border-top-color: #ddd;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
background: #303030;
|
|
border-color: #555;
|
|
color: #eee;
|
|
}
|
|
|
|
.success-message {
|
|
background: #1a3a5a;
|
|
color: #99ccff;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="navbar">
|
|
<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>
|
|
</div>
|
|
</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 data-i18n="error.title">
|
|
No meeting specified
|
|
</h1>
|
|
|
|
<div 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>
|
|
|
|
<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.</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",
|
|
/*
|
|
* 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."
|
|
},
|
|
|
|
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.",
|
|
downloadButton: "Download ICS File",
|
|
createNewButton: "Create New Meeting",
|
|
meetingNameRequired: "Please enter a meeting name",
|
|
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:
|
|
"Installiere den Jitsi Meet Desktop-Client und versuche anschließend erneut, das Meeting zu öffnen.",
|
|
button: "Jitsi für Windows herunterladen"
|
|
},
|
|
|
|
macos: {
|
|
title: "Jitsi Meet für macOS",
|
|
description:
|
|
"Installiere den Jitsi Meet Desktop-Client und versuche anschließend erneut, das Meeting zu öffnen.",
|
|
button: "Jitsi für macOS herunterladen"
|
|
},
|
|
|
|
linux: {
|
|
title: "Jitsi Meet für Linux",
|
|
description:
|
|
"Installiere den Jitsi Meet Desktop-Client und versuche anschließend erneut, das Meeting zu öffnen.",
|
|
button: "Jitsi für Linux herunterladen"
|
|
},
|
|
|
|
other: {
|
|
title: "Jitsi Meet Desktop",
|
|
description:
|
|
"Für dein Betriebssystem ist kein Desktop-Download hinterlegt."
|
|
}
|
|
},
|
|
|
|
browser: {
|
|
title: "Im Browser teilnehmen",
|
|
description:
|
|
"Du kannst dem Meeting auch direkt über deinen Browser beitreten.",
|
|
button: "Im Browser beitreten"
|
|
},
|
|
|
|
error: {
|
|
title: "Kein Meeting angegeben",
|
|
description:
|
|
"Bitte gib ein Meeting über den Parameter",
|
|
descriptionSuffix: "an."
|
|
},
|
|
|
|
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! Lade deine Kalenderdatei herunter.",
|
|
downloadButton: "ICS-Datei herunterladen",
|
|
createNewButton: "Neues Meeting erstellen",
|
|
meetingNameRequired: "Bitte gib einen Meeting-Namen ein",
|
|
startTimeRequired: "Bitte wähle eine Startzeit",
|
|
endTimeRequired: "Bitte wähle eine Endzeit",
|
|
endAfterStart: "Die Endzeit muss nach der Startzeit liegen",
|
|
organizerRequired: "Bitte gib eine Organisator-E-Mail ein",
|
|
organizerInvalid: "Bitte gib 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: "."
|
|
},
|
|
|
|
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.",
|
|
downloadButton: "Télécharger le fichier ICS",
|
|
createNewButton: "Créer une nouvelle réunion",
|
|
meetingNameRequired: "Veuillez entrer un nom de réunion",
|
|
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"
|
|
}
|
|
}
|
|
};
|
|
|
|
/* ============================================================
|
|
* 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"
|
|
};
|
|
|
|
const LANGUAGE_FLAGS = {
|
|
en: "assets/uk.svg",
|
|
de: "assets/de.svg",
|
|
fr: "assets/fr.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");
|
|
}
|
|
|
|
/* ============================================================
|
|
* 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";
|
|
}
|
|
|
|
/* ============================================================
|
|
* 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");
|
|
|
|
/* ============================================================
|
|
* 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
|
|
);
|
|
|
|
const room = params.get("room");
|
|
|
|
if (!room) {
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Leerzeichen etc. verhindern.
|
|
*/
|
|
const trimmed = room.trim();
|
|
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
/* ============================================================
|
|
* VALIDATE ROOM
|
|
* ============================================================
|
|
*
|
|
* Jitsi-Roomnamen können je nach Konfiguration relativ
|
|
* unterschiedliche Zeichen enthalten.
|
|
*
|
|
* Wir verhindern hier lediglich Steuerzeichen und
|
|
* offensichtliche URL-Manipulation.
|
|
*
|
|
* ============================================================
|
|
*/
|
|
|
|
function isValidRoom(room) {
|
|
if (!room) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Keine Steuerzeichen.
|
|
*/
|
|
if (/[\u0000-\u001F\u007F]/.test(room)) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Keine Slash-Zeichen.
|
|
*
|
|
* Falls deine Jitsi-Konfiguration Räume mit Slashes
|
|
* unterstützt, diese Prüfung entsprechend entfernen.
|
|
*/
|
|
if (room.includes("/")) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 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);
|
|
}
|
|
|
|
/* ============================================================
|
|
* ICS FILE GENERATION
|
|
* ============================================================
|
|
*/
|
|
|
|
function generateICS(meetingName, description, startTime, endTime, organizer) {
|
|
/* Generate a unique room name from the meeting name */
|
|
const roomName = meetingName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
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 (!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) {
|
|
invalidRoom.querySelector("h1").classList.add("hidden");
|
|
invalidRoom.querySelector(".error").classList.add("hidden");
|
|
invalidRoom.classList.remove("hidden");
|
|
createNewMeetingButton.classList.remove("hidden");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Ungültiger room-Parameter.
|
|
*/
|
|
if (!isValidRoom(room)) {
|
|
invalidRoom.classList.remove("hidden");
|
|
/* Show the create new meeting button when no room is specified */
|
|
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();
|
|
}
|
|
}
|
|
);
|
|
|
|
/* ============================================================
|
|
* START
|
|
* ============================================================
|
|
*/
|
|
|
|
applyTranslations();
|
|
applyBackgroundImage();
|
|
updateLanguageMenu();
|
|
initialize();
|
|
</script>
|
|
</body>
|
|
</html> |