EuerChat 0.1.0: Tauri-Client mit Direktverbindung, ZNC, Livestream, Updater und Installer-Build

This commit is contained in:
Davito committed 2026-10-14 10:21:15 +02:00
commit df88f8abaa
27 files changed
+13167

No files matched your search

+2676
View File
File diff suppressed because it is too large. Load diff
+136
View File
@@ -0,0 +1,136 @@
{
"base_url": "https://chat.euer.tv/static/emocache/",
"emotes": {
"-___-": "1f611.png",
":'-)": "1f602.png",
"':-)": "1f605.png",
"':-D": "1f605.png",
">:-)": "1f606.png",
"':-(": "1f613.png",
">:-(": "1f620.png",
":'-(": "1f622.png",
"O:-)": "1f607.png",
"0:-3": "1f607.png",
"0:-)": "1f607.png",
"0;^)": "1f607.png",
"O;-)": "1f607.png",
"0;-)": "1f607.png",
"O:-3": "1f607.png",
"-__-": "1f611.png",
":-Þ": "1f61b.png",
"<3": "2764.png",
"</3": "1f494.png",
":')": "1f602.png",
":-D": "1f603.png",
"':)": "1f605.png",
"'=)": "1f605.png",
"':D": "1f605.png",
"'=D": "1f605.png",
">:)": "1f606.png",
">;)": "1f606.png",
">=)": "1f606.png",
"XD": "1f606.png",
"xD": "1f606.png",
";-)": "1f609.png",
"*-)": "1f609.png",
";-]": "1f609.png",
";^)": "1f609.png",
"':(": "1f613.png",
"'=(": "1f613.png",
":-*": "1f618.png",
":^*": "1f618.png",
">:P": "1f61c.png",
"X-P": "1f61c.png",
">:[": "1f61e.png",
":-(": "1f61e.png",
":-[": "1f61e.png",
">:(": "1f620.png",
":'(": "1f622.png",
";-(": "1f622.png",
">.<": "1f623.png",
"#-)": "1f635.png",
"%-)": "1f635.png",
"X-)": "1f635.png",
"\\0/": "1f646.png",
"\\O/": "1f646.png",
"0:3": "1f607.png",
"0:)": "1f607.png",
"O:)": "1f607.png",
"O=)": "1f607.png",
"O:3": "1f607.png",
"B-)": "1f60e.png",
"8-)": "1f60e.png",
"B-D": "1f60e.png",
"8-D": "1f60e.png",
"-_-": "1f611.png",
">:\\": "1f615.png",
">:/": "1f615.png",
":-/": "1f615.png",
":-.": "1f615.png",
":-P": "1f61b.png",
":Þ": "1f61b.png",
":-b": "1f61b.png",
":-O": "1f62e.png",
"O_O": "1f62e.png",
">:O": "1f62e.png",
":-X": "1f636.png",
":-#": "1f636.png",
":-)": "1f642.png",
"(y)": "1f44d.png",
":D": "1f603.png",
"=D": "1f603.png",
";)": "1f609.png",
"*)": "1f609.png",
";]": "1f609.png",
";D": "1f609.png",
":*": "1f618.png",
"=*": "1f618.png",
":(": "1f61e.png",
":[": "1f61e.png",
"=(": "1f61e.png",
":@": "1f620.png",
";(": "1f622.png",
"D:": "1f628.png",
":$": "1f633.png",
"=$": "1f633.png",
"#)": "1f635.png",
"%)": "1f635.png",
"X)": "1f635.png",
"B)": "1f60e.png",
"8)": "1f60e.png",
":/": "1f615.png",
":\\": "1f615.png",
"=/": "1f615.png",
"=\\": "1f615.png",
":L": "1f615.png",
"=L": "1f615.png",
":P": "1f61b.png",
":p": "1f61b.png",
"=P": "1f61b.png",
":b": "1f61b.png",
":O": "1f62e.png",
":X": "1f636.png",
":#": "1f636.png",
"=X": "1f636.png",
"=#": "1f636.png",
":)": "1f642.png",
"=]": "1f642.png",
"=)": "1f642.png",
":]": "1f642.png",
"CATJAM": "catjam.gif",
"Clap": "clap.gif",
"AlienPls": "AlienPls.gif",
"blobDance": "blobDance.gif",
"ClapHD": "ClapHD.gif",
"Dance": "Dance.gif",
"KirbDance": "KirbDance.gif",
"PartyParrot": "PartyParrot.gif",
"pepeDS": "pepeDS.gif",
"PepoCheer": "PepoCheer.gif",
"PepoDance": "PepoDance.gif",
"ppJedi": "ppJedi.gif",
"pugPls": "pugPls.gif",
"RareParrot": "RareParrot.gif",
"xar2EDM": "xar2EDM.gif"
}
}
+4
View File
@@ -0,0 +1,4 @@
!function e(t){var r,i;r=this,i=function(){"use strict";function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,i=Array(t);r<t;r++)i[r]=e[r];return i}function i(e,t,r,i,n,a,s){try{var o=e[a](s),l=o.value}catch(e){return void r(e)}o.done?t(l):Promise.resolve(l).then(i,n)}function n(e,t){for(var r=0;r<t.length;r++){var i=t[r];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,y(i.key),i)}}function a(e,t,r){return t&&n(e.prototype,t),r&&n(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function s(e,t){var i="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(i)return(i=i.call(e)).next.bind(i);if(Array.isArray(e)||(i=function(e,t){if(e){if("string"==typeof e)return r(e,t);var i={}.toString.call(e).slice(8,-1);return"Object"===i&&e.constructor&&(i=e.constructor.name),"Map"===i||"Set"===i?Array.from(e):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?r(e,t):void 0}}(e))||t){i&&(e=i);var n=0;return function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function o(e,t,r){return(t=y(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var i in r)({}).hasOwnProperty.call(r,i)&&(e[i]=r[i])}return e},l.apply(null,arguments)}function u(e){return u=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},u(e)}function d(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,p(e,t)}function h(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(h=function(){return!!e})()}function f(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,i)}return r}function c(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?f(Object(r),!0).forEach((function(t){o(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):f(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}function g(e,t){if(null==e)return{};var r={};for(var i in e)if({}.hasOwnProperty.call(e,i)){if(-1!==t.indexOf(i))continue;r[i]=e[i]}return r}function v(){
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
var e,t,r="function"==typeof Symbol?Symbol:{},i=r.iterator||"@@iterator",n=r.toStringTag||"@@toStringTag";function a(r,i,n,a){var l=i&&i.prototype instanceof o?i:o,u=Object.create(l.prototype);return m(u,"_invoke",function(r,i,n){var a,o,l,u=0,d=n||[],h=!1,f={p:0,n:0,v:e,a:c,f:c.bind(e,4),d:function(t,r){return a=t,o=0,l=e,f.n=r,s}};function c(r,i){for(o=r,l=i,t=0;!h&&u&&!n&&t<d.length;t++){var n,a=d[t],c=f.p,g=a[2];r>3?(n=g===i)&&(l=a[(o=a[4])?5:(o=3,3)],a[4]=a[5]=e):a[0]<=c&&((n=r<2&&c<a[1])?(o=0,f.v=i,f.n=a[1]):c<g&&(n=r<3||a[0]>i||i>g)&&(a[4]=r,a[5]=i,f.n=g,o=0))}if(n||r>1)return s;throw h=!0,i}return function(n,d,g){if(u>1)throw TypeError("Generator is already running");for(h&&1===d&&c(d,g),o=d,l=g;(t=o<2?e:l)||!h;){a||(o?o<3?(o>1&&(f.n=-1),c(o,l)):f.n=l:f.v=l);try{if(u=2,a){if(o||(n="next"),t=a[n]){if(!(t=t.call(a,l)))throw TypeError("iterator result is not an object");if(!t.done)return t;l=t.value,o<2&&(o=0)}else 1===o&&(t=a.return)&&t.call(a),o<2&&(l=TypeError("The iterator does not provide a '"+n+"' method"),o=1);a=e}else if((t=(h=f.n<0)?l:r.call(i,f))!==s)break}catch(t){a=e,o=1,l=t}finally{u=1}}return{value:t,done:h}}}(r,n,a),!0),u}var s={};function o(){}function l(){}function u(){}t=Object.getPrototypeOf;var d=[][i]?t(t([][i]())):(m(t={},i,(function(){return this})),t),h=u.prototype=o.prototype=Object.create(d);function f(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,u):(e.__proto__=u,m(e,n,"GeneratorFunction")),e.prototype=Object.create(h),e}return l.prototype=u,m(h,"constructor",u),m(u,"constructor",l),l.displayName="GeneratorFunction",m(u,n,"GeneratorFunction"),m(h),m(h,n,"Generator"),m(h,i,(function(){return this})),m(h,"toString",(function(){return"[object Generator]"})),(v=function(){return{w:a,m:f}})()}function m(e,t,r,i){var n=Object.defineProperty;try{n({},"",{})}catch(e){n=0}m=function(e,t,r,i){function a(t,r){m(e,t,(function(e){return this._invoke(t,r,e)}))}t?n?n(e,t,{value:r,enumerable:!i,configurable:!i,writable:!i}):e[t]=r:(a("next",0),a("throw",1),a("return",2))},m(e,t,r,i)}function p(e,t){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},p(e,t)}function y(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var i=r.call(e,t);if("object"!=typeof i)return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}function E(e){var t="function"==typeof Map?new Map:void 0;return E=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf("[native code]")}catch(t){return"function"==typeof e}}(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return function(e,t,r){if(h())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,t);var n=new(e.bind.apply(e,i));return r&&p(n,r.prototype),n}(e,arguments,u(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),p(r,e)},E(e)}function T(e){return e instanceof ArrayBuffer||"undefined"!=typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer}var S="utf-16",A="utf-16be",I="utf-16le",L="utf-8";function R(e,t){if(e<0)return-R(-e,t);var r=Math.pow(10,t);if(Math.abs(e*r%1-.5)<Number.EPSILON){var i=Math.floor(e*r);return(i%2==0?i:i+1)/r}return Math.round(e*r)/r}function b(){try{return crypto.randomUUID()}catch(i){try{var e=URL.createObjectURL(new Blob),t=e.toString();return URL.revokeObjectURL(e),t.slice(t.lastIndexOf("/")+1)}catch(e){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){var t=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==e?t:3&t|8).toString(16)}))}}}function k(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var D,P,_={exports:{}},C=(D||(D=1,function(e){var t=Object.prototype.hasOwnProperty,r="~";function i(){}function n(e,t,r){this.fn=e,this.context=t,this.once=r||!1}function a(e,t,i,a,s){if("function"!=typeof i)throw new TypeError("The listener must be a function");var o=new n(i,a||e,s),l=r?r+t:t;return e._events[l]?e._events[l].fn?e._events[l]=[e._events[l],o]:e._events[l].push(o):(e._events[l]=o,e._eventsCount++),e}function s(e,t){0==--e._eventsCount?e._events=new i:delete e._events[t]}function o(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(r=!1)),o.prototype.eventNames=function(){var e,i,n=[];if(0===this._eventsCount)return n;for(i in e=this._events)t.call(e,i)&&n.push(r?i.slice(1):i);return Object.getOwnPropertySymbols?n.concat(Object.getOwnPropertySymbols(e)):n},o.prototype.listeners=function(e){var t=r?r+e:e,i=this._events[t];if(!i)return[];if(i.fn)return[i.fn];for(var n=0,a=i.length,s=new Array(a);n<a;n++)s[n]=i[n].fn;retLine truncated
//# sourceMappingURL=hls.min.js.map
+592
View File
@@ -0,0 +1,592 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EuerChat - euer.tv</title>
<link rel="stylesheet" href="style.css">
</head>
<body class="dark-theme">
<div id="app" class="app-container">
<!-- Top Header Bar (Optionen) -->
<header class="app-header">
<div class="header-left">
<div class="header-brand">
<span class="header-brand-title">EuerChat</span>
</div>
<!-- Nur sichtbar, wenn ein Lua-Skript (.luac) geladen und aktiv ist -->
<button class="btn-modi-switch hidden" id="btnModiToggle" title="Zwischen normalem Hörer und Radio-Moderator wechseln">
<span class="modi-dot" id="modiDot"></span>
<span id="modiToggleLabel">📻 Zum Modi wechseln</span>
</button>
</div>
<div class="header-right">
<!-- Options Button -->
<button class="btn-icon" id="btnSettingsModal" title="Optionen & Einstellungen">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
<span>Optionen</span>
</button>
</div>
</header>
<!-- Main Content Area -->
<main class="main-layout">
<!-- Chat Box (Expanded full width on the left) -->
<section class="chat-area">
<div class="messages-container" id="messagesContainer">
<!-- Messages populated dynamically -->
</div>
<!-- Chat Input Bar -->
<div class="chat-input-wrapper">
<div class="chat-input-box">
<input type="text" id="chatInput" placeholder="Nachricht an #erde... (Enter zum Senden)" autocomplete="off">
<div class="input-actions">
<button class="btn-emote" id="btnEmotePicker" title="Emotes & GIFs öffnen">
😊
</button>
<button class="btn-send" id="btnSend">
Senden
</button>
</div>
</div>
</div>
<!-- Bottom Bar: Everything related to Chat (Compact Status, Tabs, User identity) -->
<div class="bottom-bar">
<div class="bottom-chat-left">
<div class="connection-status-compact" id="connStatus" title="IRC Verbindungsstatus">
<span class="status-dot disconnected" id="statusDot"></span>
<span class="status-ping hidden" id="statusPing">-- ms</span>
</div>
<button class="btn-connect-header" id="btnHeaderConnect" title="Verbindung herstellen oder trennen">
Verbinden
</button>
</div>
<div class="channel-tabs-container">
<div class="channel-tabs" id="channelList">
<div class="channel-tab active" data-target="#erde">
<span class="chan-prefix">#</span>
<span class="chan-name">erde</span>
<span class="chan-badge unread-badge hidden">0</span>
</div>
<div class="channel-tab" data-target="*server*">
<span class="chan-prefix">ℹ</span>
<span class="chan-name">Server</span>
</div>
</div>
<button class="btn-tab-add" id="btnAddChan" title="Kanal beitreten">+</button>
</div>
<div class="bottom-user-status">
<span class="bottom-user-avatar" id="userAvatar">?</span>
<div class="bottom-user-info">
<span class="bottom-user-nick" id="displayNick">Hoerer</span>
</div>
</div>
</div>
</section>
<!-- Draggable Splitter to manually resize the user list & video player width -->
<div class="sidebar-resizer" id="sidebarResizer" title="Ziehen zum Vergrößern/Verkleinern der Leiste"></div>
<!-- Right Sidebar: User List & Embedded Video Player below it -->
<aside class="sidebar-users">
<div class="sidebar-section-title">
<span>BENUTZER</span>
<span class="user-count" id="userCount">0</span>
</div>
<div class="user-list" id="userList">
<!-- Users populated dynamically -->
</div>
<!-- Media Area: Interpret/Titel drüber, Video Player in der Mitte, Hörer & Grußbox unten -->
<div class="sidebar-media-area" id="sidebarMediaArea">
<!-- Track Info: Interpret / Titel DIREKT ÜBER DEM PLAYER (ohne Cover) -->
<div class="sidebar-track-header" id="sidebarTrackHeader" title="Aktueller Titel & Interpret">
<div class="sidebar-track-info" id="sidebarTrackInfo">
<div class="sidebar-artist" id="sidebarArtist">euer.tv Radio</div>
<div class="sidebar-track" id="sidebarTrack">Lade aktuellen Titel...</div>
</div>
</div>
<!-- Video Player Box -->
<div class="sidebar-video-box" id="sidebarVideoBox">
<div class="sidebar-video-wrapper">
<!-- Video element: MUTED AT START! -->
<video id="streamVideo" playsinline muted autoplay></video>
</div>
<!-- Video Player Controls: Mute, Volume, Quality (1080p, 720p, 360p), Resync -->
<!-- Video Player Controls: Mute, Lautstärke, Hörer, Grußbox, Qualität, Resync in EINER Zeile! -->
<div class="sidebar-player-controls" id="sidebarPlayerControls">
<div class="player-ctrls-left">
<button class="btn-video-mute is-muted" id="btnStreamMute" title="Ton stummschalten oder aktivieren">
<span id="streamMuteIcon">🔇</span>
</button>
<input type="range" id="streamVolSlider" min="0" max="1" step="0.05" value="0.1" class="mini-volume-slider" title="Lautstärke">
</div>
<div class="sidebar-listeners-display twitch-viewer-badge" id="sidebarListenersDisplay" title="Aktuelle Zuhörer*innen">
<svg class="twitch-viewer-icon" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="6.2" r="3.2"></circle>
<path d="M4.5 16.5c0-2.8 2.5-4.8 5.5-4.8s5.5 2 5.5 4.8"></path>
</svg>
<span class="stat-val" id="statTotalListeners">0</span>
</div>
<button class="btn-grussbox-compact" id="btnOpenGrussbox" title="Grußbox öffnen & Nachricht direkt ins Studio senden">
✉ Grußbox
</button>
</div>
<div class="player-ctrls-right">
<!-- Quality selector: 1080p, 720p, 360p -->
<select id="videoQualitySelect" class="video-quality-select" title="Stream-Qualität wählen">
<option value="1080p" selected>1080p</option>
<option value="720p">720p</option>
<option value="360p">360p</option>
</select>
<button class="btn-video-resync" id="btnResyncStream" title="Livestream neu synchronisieren">🔄</button>
</div>
</div>
</div>
</div>
</aside>
</main>
<!-- Emote Picker Popover -->
<div class="emote-picker hidden" id="emotePicker">
<div class="emote-picker-header">
<input type="text" id="emoteSearch" placeholder="Emote suchen (z.B. CATJAM, pepe, :) )">
<button class="btn-close-mini" id="btnCloseEmote">✕</button>
</div>
<div class="emote-picker-tabs">
<button class="emote-tab active" data-tab="all">Alle</button>
<button class="emote-tab" data-tab="gifs">GIFs</button>
<button class="emote-tab" data-tab="smilies">Smilies</button>
</div>
<div class="emote-grid" id="emoteGrid">
<!-- Emotes rendered here -->
</div>
</div>
<!-- Complete Unified Options Modal -->
<div class="modal-overlay hidden" id="settingsModal">
<div class="modal-card modal-card-wide">
<div class="modal-header">
<h2>Optionen & Einstellungen</h2>
<button class="modal-close" id="btnCloseSettingsModal">✕</button>
</div>
<div class="modal-tabs">
<button class="tab-btn active" data-tab-target="secDisplay">Darstellung</button>
<button class="tab-btn" data-tab-target="secServer">Server & ZNC</button>
<button class="tab-btn hidden" data-tab-target="secRadio" id="tabBtnRadio">Radio & Modi</button>
<button class="tab-btn" data-tab-target="secLua" id="tabBtnLua">Lua Addons</button>
<button class="tab-btn" data-tab-target="secUpdate" id="tabBtnUpdate">Updates</button>
</div>
<div class="modal-body settings-body">
<!-- 1. Display Section -->
<div class="settings-section-pane active" id="secDisplay">
<div class="settings-grid">
<div class="settings-col">
<h3 class="settings-section-title">Schrift & Text</h3>
<div class="form-group">
<label for="optFontFamily">Schriftart (System & Windows)</label>
<select id="optFontFamily" class="form-select">
<option value="system">Standard Sans (Segoe UI, System)</option>
<option value="monospace">Monospace / Konsole (Consolas, Courier)</option>
<option value="segoe">Segoe UI</option>
<option value="arial">Arial</option>
<option value="calibri">Calibri</option>
<option value="verdana">Verdana</option>
<option value="comic">Comic Sans MS</option>
<option value="trebuchet">Trebuchet MS</option>
</select>
</div>
<div class="form-group" style="margin-top: 4px;">
<label for="optFontCustom" style="font-size:11.5px;color:var(--text-dim);">Eigene installierte Schriftart manuell eingeben:</label>
<input type="text" id="optFontCustom" class="form-input" placeholder="z. B. Cascadia Code, Bahnschrift, Fira Code..." style="font-size:12px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border-color);border-radius:var(--radius-sm);color:#fff;width:100%;">
</div>
<div class="form-group">
<div class="label-with-value">
<label for="optFontSize">Schriftgröße</label>
<span class="range-val-badge" id="valFontSize">14px</span>
</div>
<input type="range" id="optFontSize" min="11" max="22" step="1" value="14" class="form-range">
</div>
<div class="form-group">
<label for="optLineHeight">Zeilenabstand & Dichte</label>
<select id="optLineHeight" class="form-select">
<option value="compact">Kompakt (eng wie mIRC / HexChat)</option>
<option value="normal" selected>Normal (ausgewogen)</option>
<option value="cozy">Komfortabel (mehr Abstand)</option>
</select>
</div>
<div class="form-group">
<label for="optTimestamp">Zeitstempel-Format</label>
<select id="optTimestamp" class="form-select">
<option value="hm" selected>HH:mm (z. B. 21:13)</option>
<option value="hms">HH:mm:ss (z. B. 21:13:45)</option>
<option value="none">Ausblenden</option>
</select>
</div>
<div class="form-group">
<label for="optUiScale">Skalierung der Oberfläche</label>
<select id="optUiScale" class="form-select">
<option value="75">75 %</option>
<option value="90">90 %</option>
<option value="100" selected>100 % (Standard)</option>
<option value="110">110 %</option>
<option value="125">125 %</option>
<option value="150">150 %</option>
<option value="175">175 %</option>
<option value="200">200 %</option>
</select>
</div>
</div>
<div class="settings-col">
<h3 class="settings-section-title">Chat & Emotes</h3>
<div class="form-group">
<label for="optEmoteSize">Emote-Größe</label>
<select id="optEmoteSize" class="form-select">
<option value="20">Klein (20px)</option>
<option value="26" selected>Normal (26px)</option>
<option value="34">Groß (34px)</option>
<option value="42">Riesig (42px)</option>
</select>
</div>
<div class="settings-checkboxes">
<label class="form-checkbox">
<input type="checkbox" id="optIrcColors" checked>
<span>mIRC Farbcodes & Formatierung anzeigen</span>
</label>
<label class="form-checkbox">
<input type="checkbox" id="optEmotes" checked>
<span>Emotes & animierte GIFs ersetzen</span>
</label>
<label class="form-checkbox">
<input type="checkbox" id="optHighlight" checked>
<span>Eigenen Nicknamen hervorheben</span>
</label>
<label class="form-checkbox">
<input type="checkbox" id="optAutoScroll" checked>
<span>Automatisch nach unten scrollen</span>
</label>
</div>
<div class="settings-checkboxes" style="margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-color);">
<span class="preview-title" style="display:block; margin-bottom: 6px; font-size:10.5px; color:var(--text-dim); font-weight:700;">SICHTBARKEIT & LAYOUT</span>
<label class="form-checkbox">
<input type="checkbox" id="optShowMediaArea" checked>
<span>Livestream-Player, Hörer & Grußbox anzeigen</span>
</label>
</div>
</div>
</div>
<!-- Live Preview -->
<div class="settings-preview-wrapper">
<span class="preview-title">VORSCHAU</span>
<div class="settings-preview" id="settingsPreview">
<div class="chat-msg">
<span class="msg-time">21:13</span>
<span class="msg-nick" style="color:#48dbfb;">DJ_Radio</span>
<span class="msg-text">Hallo zusammen! Viel Spaß mit der Musik <img class="chat-emote" src="https://chat.euer.tv/static/emocache/catjam.gif" alt="CATJAM"></span>
</div>
<div class="chat-msg action-msg">
<span class="msg-time">21:14</span>
<span class="msg-text"><span class="msg-action-star">*</span> <span class="msg-nick" style="color:#1dd1a1;">Kellnerin</span> <span style="color:#facc15;">Jetzt läuft:</span> <span style="color:#38bdf8;">Usher Feat. Pitbull</span> - <span style="color:#a855f7;">Dj Got Us Fallin' In Love</span></span>
</div>
</div>
</div>
</div>
<!-- 2. Server & ZNC Section -->
<div class="settings-section-pane hidden" id="secServer">
<div class="server-type-selector">
<button class="server-type-btn active" id="btnTypeDirect">Direkt (irc.euer.tv)</button>
<button class="server-type-btn" id="btnTypeZnc">ZNC Bouncer</button>
</div>
<!-- Direct Form -->
<form id="formDirect" class="form-sub-section active">
<p class="section-desc">Verbindet direkt mit dem Radio-Chat <strong>irc.euer.tv</strong> (Port 6697, SSL-verschlüsselt) im Raum <strong>#erde</strong>.</p>
<div class="form-group">
<label for="directNick">Name (Nickname)</label>
<input type="text" id="directNick" placeholder="Dein Name im Chat" maxlength="30" autocomplete="off" required>
</div>
<div class="form-group">
<label for="directPass">Passwort (optional, nur mit registriertem Account)</label>
<input type="password" id="directPass" placeholder="Leer lassen, wenn dein Name nicht registriert ist" autocomplete="current-password">
</div>
<div class="form-checkbox">
<input type="checkbox" id="directAutoConnect">
<label for="directAutoConnect">Automatisch beim Start direkt verbinden</label>
</div>
<div class="server-connect-action">
<button type="button" class="btn-success" id="btnSaveDirect" title="Direkt-Einstellungen speichern">💾 Direkt Speichern</button>
<button type="button" class="btn-primary" id="btnConnectNow" title="Mit diesen Daten direkt verbinden">⚡ Jetzt Verbinden</button>
<button type="button" class="btn-secondary" id="btnClearDirect" title="Alle Direkt-Felder leeren">🗑️ Leeren</button>
</div>
<div class="config-save-feedback hidden" id="directSaveFeedback"></div>
</form>
<!-- ZNC Form -->
<form id="formZnc" class="form-sub-section hidden">
<div class="form-row">
<div class="form-group flex-2">
<label>ZNC Host / IP</label>
<input type="text" id="zncHost" placeholder="z. B. meinserver.example.org" required>
</div>
<div class="form-group flex-1">
<label>ZNC Port</label>
<input type="number" id="zncPort" value="8250" placeholder="8250" required>
</div>
</div>
<div class="form-row">
<div class="form-group flex-1">
<label>ZNC Benutzer (z.B. user oder user/network)</label>
<input type="text" id="zncUser" placeholder="z. B. benutzer oder benutzer/netzwerk" required>
</div>
<div class="form-group flex-1">
<label>ZNC Passwort</label>
<input type="password" id="zncPass" placeholder="ZNC Passwort" required>
</div>
</div>
<div class="form-row">
<div class="form-group flex-1">
<label>Nickname</label>
<input type="text" id="zncNick" placeholder="Dein Nickname">
</div>
<div class="form-group flex-1">
<label>Standard-Raum</label>
<input type="text" id="zncChannel" value="#erde">
</div>
</div>
<div class="form-checkbox">
<input type="checkbox" id="zncSsl">
<label for="zncSsl">SSL / TLS Verschlüsselung aktivieren</label>
</div>
<div class="form-checkbox">
<input type="checkbox" id="zncAutoConnect">
<label for="zncAutoConnect">Automatisch beim Start über ZNC verbinden</label>
</div>
<div class="server-connect-action">
<button type="button" class="btn-success" id="btnSaveZnc" title="ZNC-Einstellungen speichern">💾 ZNC Speichern</button>
<button type="button" class="btn-primary" id="btnConnectZncNow" title="Über ZNC verbinden und als Standard speichern">⚡ Über ZNC Verbinden</button>
<button type="button" class="btn-secondary" id="btnClearZnc" title="Alle ZNC-Felder leeren">🗑️ Leeren</button>
</div>
<div class="config-save-feedback hidden" id="zncSaveFeedback"></div>
</form>
</div>
<!-- 3. Radio & Modi Section -->
<div class="settings-section-pane hidden" id="secRadio">
<h3 class="settings-section-title">Radio & Sende-Modus (Modi-Verwaltung)</h3>
<p class="section-desc">
Konfiguriere hier deinen normalen Hörer-Nick und deinen Moderatoren-Nick. Ein Klick auf den Modi-Button oben schaltet automatisch um:
</p>
<div class="form-row">
<div class="form-group flex-1">
<label for="cfgNormalNick">Normaler Nickname (Hörer)</label>
<input type="text" id="cfgNormalNick" placeholder="Dein normaler Nickname">
</div>
<div class="form-group flex-1">
<label for="cfgModiNick">Modi-Nickname (Auf Sendung)</label>
<input type="text" id="cfgModiNick" placeholder="z. B. DJ_Name">
</div>
</div>
<div class="form-group">
<label for="cfgInternPass">Passwort für Raum #intern</label>
<input type="password" id="cfgInternPass" placeholder="Passwort (leer lassen, falls kein Schlüssel nötig)" autocomplete="off">
<small class="field-hint">Wird beim Wechsel zum Modi automatisch für <code>JOIN #intern &lt;passwort&gt;</code> verwendet.</small>
</div>
<div class="modi-status-preview-box">
<div class="modi-preview-item">
<span class="preview-badge normal">🎧 Normal</span>
<span>Nur im Raum <strong>#erde</strong> unterwegs.</span>
</div>
<div class="modi-preview-item">
<span class="preview-badge modi">📻 Modi Aktiv</span>
<span>Nick wechselt zu Moderatoren-Name & betritt automatisch <strong>#intern</strong>.</span>
</div>
</div>
</div>
<!-- 4. Lua Addons Section (Clean & Uncluttered by default) -->
<div class="settings-section-pane hidden" id="secLua">
<h3 class="settings-section-title">LUA ERWEITERUNGEN (*.LUA, *.LUAC)</h3>
<p class="section-desc" style="font-size:12.5px;color:var(--text-muted);margin-bottom:12px;">
Hier können optionale Skripte geladen werden.
</p>
<div class="script-upload-box" id="luaPickerBox">
<input type="file" id="luaFileInput" accept=".lua,.luac" style="display:none">
<button type="button" class="btn-secondary" id="btnSelectLua">
📁 Externe .luac / .lua Datei auswählen...
</button>
<span class="selected-file-name" id="luaFileName">Keine Datei ausgewählt</span>
</div>
<div class="loaded-scripts-list" id="loadedScriptsList">
<div class="script-item" id="scriptStatusText" style="color:var(--text-muted);font-size:12.5px;padding:6px;">
○ Inaktiv (Kein Skript geladen)
</div>
</div>
<!-- Radio Moderations-Details: NUR SICHTBAR WENN EIN SKRIPT GELADEN WURDE! -->
<div class="lua-modi-details hidden" id="luaModiDetails" style="margin-top:14px;border-top:1px solid var(--border-color);padding-top:14px;">
<div class="form-checkbox" style="margin-bottom:14px;">
<input type="checkbox" id="chkEnableLua">
<label for="chkEnableLua"><strong>EuerService Moderations-Addon aktiv</strong> (schaltet Modi-Umschalter frei)</label>
</div>
</div>
</div>
<!-- 5. Updates Section -->
<div class="settings-section-pane hidden" id="secUpdate">
<h3 class="settings-section-title">Updates</h3>
<p class="section-desc">Installierte Version: <strong id="updCurrentVersion">–</strong></p>
<div class="form-checkbox">
<input type="checkbox" id="optAutoUpdate" checked>
<label for="optAutoUpdate">Beim Start automatisch nach Updates suchen</label>
</div>
<div class="server-connect-action">
<button type="button" class="btn-primary" id="btnCheckUpdate">Auf Updates prüfen</button>
</div>
<div class="update-status" id="updStatusText"></div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="btnResetSettings">Standard</button>
<button class="btn-primary" id="btnSaveSettings">Übernehmen & Schließen</button>
</div>
</div>
</div>
<!-- Grußbox Modal -->
<div class="modal-overlay hidden" id="grussboxModal">
<div class="modal-card modal-card-sm">
<div class="modal-header">
<h2>✉ Grußbox - Radio euer.tv</h2>
<button class="modal-close" id="btnCloseGrussbox">✕</button>
</div>
<div class="modal-body grussbox-body">
<p class="grussbox-desc">Sende deinen Gruß oder Musikwunsch direkt live ins Studio zum aktuellen Moderator!</p>
<div class="form-group">
<label for="gbNameInput">Dein Name / Nickname</label>
<input type="text" id="gbNameInput" class="form-input" placeholder="Dein Name" maxlength="40">
</div>
<div class="form-group">
<label for="gbTextInput">Dein Gruß / Musikwunsch</label>
<textarea id="gbTextInput" class="form-textarea" rows="4" placeholder="Schreib hier deinen Gruß oder Musikwunsch..." maxlength="500"></textarea>
</div>
<div class="grussbox-feedback hidden" id="gbFeedback"></div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="btnCancelGruss">Abbrechen</button>
<button class="btn-primary btn-submit-gruss" id="btnSubmitGruss">🚀 Gruß senden</button>
</div>
</div>
</div>
<!-- Update Modal -->
<div class="modal-overlay hidden" id="updateModal">
<div class="modal-card modal-card-sm">
<div class="modal-header">
<h2>⬆ Update verfügbar</h2>
<button class="modal-close" id="btnCloseUpdate">✕</button>
</div>
<div class="modal-body update-body">
<p class="update-summary" id="updSummary"></p>
<span class="preview-title">WAS IST NEU?</span>
<div class="update-notes" id="updNotes"></div>
<div class="update-progress hidden" id="updProgress"><div class="update-progress-bar" id="updProgressBar"></div></div>
<div class="update-status" id="updModalStatus"></div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="btnUpdateLater">Später</button>
<button class="btn-primary" id="btnUpdateInstall">Jetzt installieren</button>
</div>
</div>
</div>
<!-- User Context Menu Popover (Einfacher Klick auf Nicknamen) -->
<div class="user-context-menu hidden" id="userContextMenu">
<div class="ctx-header">
<span class="ctx-prefix" id="ctxUserPrefix"></span>
<span class="ctx-nick" id="ctxUserNick">Nick</span>
</div>
<div class="ctx-list">
<button class="ctx-item" data-action="query">
<span class="ctx-icon">💬</span> Privatnachricht (PN)
</button>
<button class="ctx-item" data-action="whois">
<span class="ctx-icon">ℹ</span> WHOIS Info
</button>
<button class="ctx-item" data-action="grussbox">
<span class="ctx-icon">✉</span> In Grußbox erwähnen
</button>
<button class="ctx-item" data-action="copy">
<span class="ctx-icon">📋</span> Nickname kopieren
</button>
<div class="ctx-divider"></div>
<div class="ctx-subhead">Kanal-Moderation</div>
<button class="ctx-item" data-action="op">
<span class="ctx-icon">⚡</span> OP geben (+o)
</button>
<button class="ctx-item" data-action="deop">
<span class="ctx-icon">⚡</span> OP entziehen (-o)
</button>
<button class="ctx-item" data-action="voice">
<span class="ctx-icon">🎤</span> Voice geben (+v)
</button>
<button class="ctx-item" data-action="devoice">
<span class="ctx-icon">🎤</span> Voice entziehen (-v)
</button>
<div class="ctx-divider"></div>
<button class="ctx-item ctx-danger" data-action="kick">
<span class="ctx-icon">👢</span> Kicken
</button>
<button class="ctx-item ctx-danger" data-action="ban">
<span class="ctx-icon">⛔</span> Kicken & Bannen
</button>
</div>
</div>
</div>
<script src="hls.min.js"></script>
<script src="app.js"></script>
</body>
</html>
+2283
View File
File diff suppressed because it is too large. Load diff