many updates

This commit is contained in:
2026-02-11 11:38:26 +11:00
parent 8cb9e43a72
commit b069d5bee8
21 changed files with 1324 additions and 85 deletions

59
.drone.yml Normal file
View File

@@ -0,0 +1,59 @@
kind: pipeline
type: docker
name: default
trigger:
event:
- push
- pull_request
steps:
- name: go-test
image: cache.coadcorp.com/library/golang
commands:
- go mod download
- go test ./...
- name: go-build
image: cache.coadcorp.com/library/golang
commands:
- go build -v ./...
- name: dockerfile-lint
image: cache.coadcorp.com/library/hadolint/hadolint:v2.12.0-alpine
commands:
- hadolint -t error Dockerfile
- name: compose-validate
image: cache.coadcorp.com/library/docker:27-cli
commands:
- apk add --no-cache docker-cli-compose
- docker compose -f docker-compose.yml config -q
- docker compose -f docker-compose.host.yml config -q
- name: docker-build-validate
image: gcr.io/kaniko-project/executor:v1.23.2-debug
commands:
- /kaniko/executor --context "${DRONE_WORKSPACE}" --dockerfile "${DRONE_WORKSPACE}/Dockerfile" --no-push --destination xteve:validate --build-arg TARGETOS=linux --build-arg TARGETARCH=amd64
when:
event:
- pull_request
- name: docker-publish
image: plugins/docker
settings:
registry: registry.coadcorp.com
repo: registry.coadcorp.com/nathan/xteve
dockerfile: Dockerfile
username: nathan
password:
from_secret: registry_password
tags:
- latest
- ${DRONE_COMMIT_SHA}
build_args:
- TARGETOS=linux
- TARGETARCH=amd64
when:
event:
- push

View File

@@ -15,14 +15,14 @@
<body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);"> <body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
<div id="loading" class="block"> <div id="loading" class="block" role="status" aria-live="polite" aria-label="Loading" aria-hidden="false">
<div class="loader"></div> <div class="loader"></div>
</div> </div>
<div id="header" class="imgCenter"></div> <div id="header" class="imgCenter"></div>
<div id="box"> <main id="box" role="main" aria-labelledby="head-text">
<table id="clientInfo" class="visible"> <table id="clientInfo" class="visible" aria-label="Server information">
<tr> <tr>
<td class="tdKey">Version:</td> <td class="tdKey">Version:</td>
<td id="version" class="tdVal">&nbsp;</td> <td id="version" class="tdVal">&nbsp;</td>
@@ -46,13 +46,14 @@
<div id="headline"> <div id="headline">
<h1 id="head-text" class="center">Configuration</h1> <h1 id="head-text" class="center">Configuration</h1>
</div> </div>
<p id="err" class="errorMsg center"></p> <p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
<div id="content"> <div id="content" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Configuration step">
</div> </div>
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
<div id="box-footer"> <div id="box-footer">
<input id="next" class="" type="button" name="next" value="Next" onclick="javascript: saveWizard();"> <input id="next" class="" type="button" name="next" value="Next" aria-controls="content" onclick="javascript: saveWizard();">
</div> </div>
</div> </main>
</body> </body>
</html> </html>

View File

@@ -14,34 +14,34 @@
<div id="header" class="imgCenter"></div> <div id="header" class="imgCenter"></div>
<div id="box"> <main id="box" role="main" aria-labelledby="head-text">
<div id="headline"> <div id="headline">
<h1 id="head-text" class="center">{{.account.headline}}</h1> <h1 id="head-text" class="center">{{.account.headline}}</h1>
</div> </div>
<p id="err" class="errorMsg center"></p> <p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
<div id="content"> <div id="content">
<form id="authentication" action="" method="post"> <form id="authentication" action="" method="post" aria-describedby="err" novalidate>
<h5>{{.account.username.title}}:</h5> <label for="username">{{.account.username.title}}:</label>
<input id="username" type="text" name="username" placeholder="Username" value=""> <input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
<h5>{{.account.password.title}}:</h5> <label for="password">{{.account.password.title}}:</label>
<input id="password" type="password" name="password" placeholder="Password" value=""> <input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="new-password">
<h5>{{.account.confirm.title}}:</h5> <label for="confirm">{{.account.confirm.title}}:</label>
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value=""> <input id="confirm" type="password" name="confirm" placeholder="Confirm" value="" autocomplete="new-password">
</form> </form>
</div> </div>
<div id="box-footer"> <div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" onclick="javascript: login();"> <input id="submit" class="" type="button" value="{{.button.craeteAccount}}" aria-label="{{.button.craeteAccount}}" onclick="javascript: login();">
</div> </div>
</div> </main>
</body> </body>
</html> </html>

View File

@@ -7,7 +7,7 @@
--line: #274462; --line: #274462;
--line-soft: #1b334d; --line-soft: #1b334d;
--text: #e9f5ff; --text: #e9f5ff;
--text-muted: #9db5cb; --text-muted: #b7cee1;
--accent: #35d2ff; --accent: #35d2ff;
--accent-strong: #12b9ff; --accent-strong: #12b9ff;
--accent-soft: rgba(53, 210, 255, 0.2); --accent-soft: rgba(53, 210, 255, 0.2);
@@ -152,6 +152,11 @@ button {
font-size: 13px; font-size: 13px;
} }
:focus-visible {
outline: 2px solid #91eaff;
outline-offset: 2px;
}
input, input,
select { select {
margin: 4px 0; margin: 4px 0;
@@ -220,6 +225,21 @@ button:active {
transform: translateY(0); transform: translateY(0);
} }
input[type=button]:focus-visible,
input[type=submit]:focus-visible,
button:focus-visible,
input[type=checkbox]:focus-visible,
a:focus-visible,
select:focus-visible,
textarea:focus-visible,
input[type=text]:focus-visible,
input[type=search]:focus-visible,
input[type=password]:focus-visible,
input[type=number]:focus-visible {
outline: 2px solid #7de5ff;
outline-offset: 2px;
}
input[type=button].delete { input[type=button].delete {
color: #fff; color: #fff;
background: linear-gradient(135deg, #ff7f7f 0%, #e94343 100%); background: linear-gradient(135deg, #ff7f7f 0%, #e94343 100%);
@@ -344,6 +364,37 @@ input[type=checkbox]:checked:before {
text-align: center; text-align: center;
} }
.skip-link {
position: absolute;
top: 10px;
left: 10px;
z-index: 2600;
border-radius: 999px;
padding: 8px 12px;
font-size: 12px;
font-weight: 700;
color: #03131f;
background: linear-gradient(135deg, #7be8ff 0%, #19c1ff 100%);
transform: translateY(-180%);
transition: transform 0.2s ease;
}
.skip-link:focus-visible {
transform: translateY(0);
}
.sr-only {
border: 0 !important;
clip: rect(0 0 0 0) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 1px !important;
}
.floatRight { .floatRight {
float: right; float: right;
} }
@@ -410,7 +461,7 @@ input[type=checkbox]:checked:before {
} }
.errorMsg { .errorMsg {
color: var(--error); color: #ff8d8d;
} }
.warningMsg { .warningMsg {
@@ -505,6 +556,43 @@ input[type=checkbox]:checked:before {
color: #d5efff; color: #d5efff;
} }
.popup-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.popup-title h3 {
margin: 0;
text-align: left;
}
#popup-custom .popup-title h3 {
margin: 0;
text-align: left;
}
.popup-close {
min-width: 40px;
min-height: 40px;
margin: 0;
padding: 0;
border-radius: 999px;
border: 1px solid var(--line);
color: #d9f0ff;
background: rgba(14, 32, 50, 0.9);
box-shadow: none;
font-size: 20px;
line-height: 1;
}
.popup-close:hover {
color: #ffffff;
border-color: #5bc8ed;
}
#popup-custom table, #popup-custom table,
#content_settings table, #content_settings table,
#mapping-detail-table, #mapping-detail-table,
@@ -637,20 +725,58 @@ input[type=checkbox]:checked:before {
} }
#popup { #popup {
padding: 10px; padding: 0;
} }
#popup-custom, #popup-custom,
#mapping-detail, #mapping-detail,
#user-detail, #user-detail,
#file-detail { #file-detail {
max-height: calc(100vh - 20px); max-width: none;
padding: 12px; width: 100%;
height: 100%;
max-height: 100%;
border-radius: 0;
border: 0;
padding: 10px 10px calc(14px + env(safe-area-inset-bottom));
}
#popup-custom table {
border-spacing: 0 10px;
}
#popup-custom tr {
display: block;
border: 1px solid var(--line-soft);
border-radius: 10px;
padding: 8px 8px 10px;
background: rgba(12, 26, 42, 0.8);
}
#popup-custom td {
display: block;
width: 100%;
padding: 4px 2px;
}
#popup-custom td.left {
width: 100%;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
#popup-interaction {
position: sticky;
bottom: 0;
z-index: 5;
margin-top: 10px;
padding-top: 10px;
background: linear-gradient(180deg, rgba(12, 27, 44, 0.1) 0%, rgba(12, 27, 44, 0.96) 30%);
} }
.interaction, .interaction,
#interaction, #interaction {
#popup-interaction {
justify-content: stretch; justify-content: stretch;
} }
@@ -658,5 +784,15 @@ input[type=checkbox]:checked:before {
.interaction input[type=submit], .interaction input[type=submit],
#popup-interaction input[type=button] { #popup-interaction input[type=button] {
width: 100%; width: 100%;
min-height: 44px;
}
.popup-title h3 {
font-size: 1.02rem;
}
.popup-close {
min-width: 44px;
min-height: 44px;
} }
} }

View File

@@ -74,6 +74,12 @@
transform: translateX(2px); transform: translateX(2px);
} }
#main-menu li:focus-visible {
border-color: rgba(103, 232, 255, 0.62);
transform: translateX(2px);
box-shadow: 0 0 0 3px rgba(53, 210, 255, 0.23);
}
#main-menu li.menu-active { #main-menu li.menu-active {
border-color: rgba(103, 232, 255, 0.55); border-color: rgba(103, 232, 255, 0.55);
background: linear-gradient(135deg, #6fe6ff 0%, #1cc5ff 100%); background: linear-gradient(135deg, #6fe6ff 0%, #1cc5ff 100%);
@@ -321,6 +327,7 @@ nav p {
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
position: relative;
} }
#content-interaction .search { #content-interaction .search {
@@ -328,6 +335,10 @@ nav p {
margin-left: auto; margin-left: auto;
} }
.mobile-only-control {
display: none;
}
#box-wrapper { #box-wrapper {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
@@ -368,6 +379,19 @@ nav p {
background-color: rgba(28, 53, 79, 0.5); background-color: rgba(28, 53, 79, 0.5);
} }
#content_table tr[tabindex],
#content_table td[tabindex] {
cursor: pointer;
}
#content_table tr[tabindex]:focus-visible,
#content_table td[tabindex]:focus-visible,
.keyboard-clickable:focus-visible {
outline: 2px solid #7de5ff;
outline-offset: -2px;
background-color: rgba(39, 73, 106, 0.56);
}
#content_table td { #content_table td {
padding: 7px 8px; padding: 7px 8px;
vertical-align: middle; vertical-align: middle;
@@ -562,6 +586,7 @@ nav p {
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
#layout { #layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding-top: max(10px, env(safe-area-inset-top));
} }
#layout-overlay { #layout-overlay {
@@ -582,28 +607,49 @@ nav p {
.layout-left { .layout-left {
position: fixed; position: fixed;
left: 12px; left: 0;
top: 12px; top: 0;
bottom: 12px; bottom: 0;
width: min(300px, calc(100vw - 42px)); width: min(320px, calc(100vw - 34px));
max-width: none; max-width: none;
z-index: 1100; z-index: 1100;
transform: translateX(-116%); transform: translateX(-116%);
transition: transform 0.25s ease; transition: transform 0.25s ease;
border-radius: 0 16px 16px 0;
border-left: 0;
} }
body.menu-open .layout-left { body.menu-open .layout-left {
transform: translateX(0); transform: translateX(0);
} }
#main-menu li {
min-height: 48px;
padding: 10px 12px;
}
#main-menu li p {
font-size: 13px;
}
.layout-right { .layout-right {
min-height: calc(100vh - 24px); min-height: calc(100vh - 24px);
} }
#shell-header {
position: sticky;
top: 0;
z-index: 8;
backdrop-filter: blur(8px);
}
#menu-toggle { #menu-toggle {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 42px;
min-width: 92px;
font-size: 13px;
} }
#clientInfo, #clientInfo,
@@ -614,6 +660,24 @@ nav p {
#content { #content {
padding: 10px; padding: 10px;
padding-bottom: calc(14px + env(safe-area-inset-bottom));
}
#content-interaction {
position: sticky;
top: 10px;
z-index: 6;
margin-bottom: 10px;
padding: 8px;
border: 1px solid var(--line-soft);
border-radius: 12px;
background: rgba(9, 22, 36, 0.92);
backdrop-filter: blur(6px);
}
#content-interaction input[type=button],
#content-interaction select {
min-height: 42px;
} }
#allStreams { #allStreams {
@@ -630,6 +694,118 @@ nav p {
} }
} }
@media only screen and (max-width: 760px) {
#shell-header {
padding: 10px 10px;
}
#clientInfo {
display: grid;
grid-template-columns: minmax(86px, 34%) 1fr;
gap: 7px 8px;
border-spacing: 0;
white-space: normal;
background: rgba(8, 20, 33, 0.44);
}
#clientInfo tr {
display: contents;
}
#clientInfo .tdKey {
text-align: right;
font-size: 9px;
letter-spacing: 0.08em;
}
#clientInfo .tdVal {
font-size: 11px;
min-height: 34px;
display: flex;
align-items: center;
}
.dashboard-cards {
grid-template-columns: repeat(2, minmax(120px, 1fr));
}
#content_table {
display: block;
}
#content_table .content_table_header {
display: none;
}
#content_table tbody,
#content_table tr,
#content_table td {
display: block;
width: 100%;
}
#content_table tr {
border: 1px solid var(--line-soft);
border-left-width: 3px;
border-radius: 12px;
margin-bottom: 10px;
padding: 6px;
background: rgba(11, 24, 39, 0.86);
}
#content_table td {
border: 0;
border-radius: 10px;
margin: 4px 0;
padding: 7px 8px;
display: grid;
grid-template-columns: minmax(92px, 36%) 1fr;
align-items: center;
gap: 8px;
background: rgba(17, 34, 53, 0.5);
}
#content_table td::before {
content: attr(data-label);
color: #8bb9d7;
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
#content_table td[data-cell-type="checkbox"],
#content_table td[data-cell-type="image"] {
grid-template-columns: 1fr;
justify-items: start;
}
#content_table td[data-cell-type="checkbox"]::before,
#content_table td[data-cell-type="image"]::before {
margin-bottom: 4px;
}
#content_table td[data-cell-type="image"] img {
margin-left: 0;
}
#content_table input[type=text] {
min-width: 0;
max-width: none;
width: 100%;
min-height: 40px;
}
#content_table input[type=checkbox] {
width: 24px;
height: 24px;
}
.mobile-only-control {
display: block;
width: 100%;
}
}
@media only screen and (max-width: 620px) { @media only screen and (max-width: 620px) {
#layout { #layout {
padding: 8px; padding: 8px;
@@ -655,19 +831,10 @@ nav p {
} }
#clientInfo { #clientInfo {
display: block; grid-template-columns: minmax(78px, 32%) 1fr;
overflow-x: auto; gap: 6px;
white-space: nowrap; padding-left: 8px;
} padding-right: 8px;
#clientInfo table,
#clientInfo tbody,
#clientInfo tr {
width: auto;
}
.dashboard-cards {
grid-template-columns: repeat(2, minmax(120px, 1fr));
} }
#content-interaction { #content-interaction {
@@ -683,6 +850,7 @@ nav p {
#content-interaction input[type=button] { #content-interaction input[type=button] {
width: 100%; width: 100%;
margin-right: 0; margin-right: 0;
min-height: 44px;
} }
#box { #box {

View File

@@ -17,15 +17,17 @@
<body class="app-shell" onload="javascript: PageReady();"> <body class="app-shell" onload="javascript: PageReady();">
<div id="loading" class="none"> <a class="skip-link" href="#content">Skip to main content</a>
<div id="loading" class="none" role="status" aria-live="polite" aria-label="Loading" aria-hidden="true">
<div class="loader"></div> <div class="loader"></div>
</div> </div>
<div id="popup" class="none"> <div id="popup" class="none" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1">
<div id="popup-custom"></div> <div id="popup-custom" role="document" tabindex="-1"></div>
</div> </div>
<div id="layout-overlay"></div> <div id="layout-overlay" aria-hidden="true" tabindex="-1"></div>
<div id="layout"> <div id="layout">
<!-- <!--
@@ -39,20 +41,20 @@
</div> </div>
--> -->
<aside id="menu-wrapper" class="layout-left"> <aside id="menu-wrapper" class="layout-left" aria-label="Sidebar menu">
<div id= "branch"></div> <div id= "branch"></div>
<div id="logo"></div> <div id="logo"></div>
<nav id="main-menu"></nav> <nav id="main-menu" role="menubar" aria-label="Main navigation"></nav>
</aside> </aside>
<main class="layout-right"> <main id="shell-main" class="layout-right">
<header id="shell-header"> <header id="shell-header">
<button id="menu-toggle" type="button">Menu</button> <button id="menu-toggle" type="button" aria-expanded="false" aria-controls="menu-wrapper" aria-label="Toggle navigation menu">Menu</button>
<h2 id="shell-title">xTeVe Control Panel</h2> <h2 id="shell-title">xTeVe Control Panel</h2>
<p id="connection-indicator" class="status-idle">Connecting...</p> <p id="connection-indicator" class="status-idle" role="status" aria-live="polite" aria-atomic="true">Connecting...</p>
</header> </header>
<table id="clientInfo" class=""> <table id="clientInfo" class="" aria-label="Server information">
<tr> <tr>
<td class="tdKey">xTeVe:</td> <td class="tdKey">xTeVe:</td>
@@ -92,9 +94,9 @@
</table> </table>
<div id="status-cards" class="dashboard-cards"></div> <div id="status-cards" class="dashboard-cards" role="list" aria-live="polite" aria-label="System summary"></div>
<div id="myStreamsBox" class="notVisible"> <div id="myStreamsBox" class="notVisible" aria-live="polite" aria-label="Stream details">
<div id="allStreams"> <div id="allStreams">
<table id="activeStreams"></table> <table id="activeStreams"></table>
@@ -103,7 +105,8 @@
</div> </div>
<div id="content" class=""></div> <div id="content" class="" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Main content"></div>
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
</main> </main>

View File

@@ -1,8 +1,13 @@
function login() { function login() {
var err = false; var err = false;
var firstInvalid = null;
var data = new Object(); var data = new Object();
var div = document.getElementById("content"); var div = document.getElementById("content");
var form = document.getElementById("authentication"); var form = document.getElementById("authentication");
var errElement = document.getElementById("err");
if (errElement != null) {
errElement.innerHTML = "";
}
var inputs = div.getElementsByTagName("INPUT"); var inputs = div.getElementsByTagName("INPUT");
console.log(inputs); console.log(inputs);
for (var i = inputs.length - 1; i >= 0; i--) { for (var i = inputs.length - 1; i >= 0; i--) {
@@ -10,14 +15,22 @@ function login() {
var value = inputs[i].value; var value = inputs[i].value;
if (value.length == 0) { if (value.length == 0) {
inputs[i].style.borderColor = "red"; inputs[i].style.borderColor = "red";
inputs[i].setAttribute("aria-invalid", "true");
if (firstInvalid == null) {
firstInvalid = inputs[i];
}
err = true; err = true;
} }
else { else {
inputs[i].style.borderColor = ""; inputs[i].style.borderColor = "";
inputs[i].setAttribute("aria-invalid", "false");
} }
data[key] = value; data[key] = value;
} }
if (err == true) { if (err == true) {
if (firstInvalid != null) {
firstInvalid.focus();
}
data = new Object(); data = new Object();
return; return;
} }
@@ -25,10 +38,31 @@ function login() {
if (data["confirm"] != data["password"]) { if (data["confirm"] != data["password"]) {
document.getElementById('password').style.borderColor = "red"; document.getElementById('password').style.borderColor = "red";
document.getElementById('confirm').style.borderColor = "red"; document.getElementById('confirm').style.borderColor = "red";
document.getElementById('password').setAttribute("aria-invalid", "true");
document.getElementById('confirm').setAttribute("aria-invalid", "true");
document.getElementById("err").innerHTML = "{{.account.failed}}"; document.getElementById("err").innerHTML = "{{.account.failed}}";
document.getElementById('password').focus();
return; return;
} }
} }
console.log(data); console.log(data);
form.submit(); form.submit();
} }
document.addEventListener("DOMContentLoaded", function () {
var form = document.getElementById("authentication");
if (form == null) {
return;
}
var inputs = form.getElementsByTagName("INPUT");
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener("keydown", function (event) {
if (event.key == "Enter") {
event.preventDefault();
login();
}
});
}
if (inputs.length > 0) {
inputs[0].focus();
}
});

View File

@@ -6,6 +6,7 @@ var UNDO = new Object();
var SERVER_CONNECTION = false; var SERVER_CONNECTION = false;
var WS_AVAILABLE = false; var WS_AVAILABLE = false;
var ACTIVE_MENU_ID = ""; var ACTIVE_MENU_ID = "";
var LAST_FOCUSED_ELEMENT = null;
// Menü // Menü
var menuItems = new Array(); var menuItems = new Array();
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}")); menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
@@ -19,7 +20,7 @@ menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}")); menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
// Kategorien für die Einstellungen // Kategorien für die Einstellungen
var settingsCategory = new Array(); var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
@@ -50,6 +51,42 @@ function showElement(elmID, type) {
return; return;
} }
element.className = cssClass; element.className = cssClass;
element.setAttribute("aria-hidden", type == true ? "false" : "true");
if (elmID == "loading" && document.body != null) {
document.body.setAttribute("aria-busy", type == true ? "true" : "false");
}
if (elmID == "popup") {
var popupContent_1 = document.getElementById("popup-custom");
if (type == true) {
LAST_FOCUSED_ELEMENT = document.activeElement;
if (popupContent_1 != null) {
setTimeout(function () {
popupContent_1.focus();
}, 20);
}
}
else {
if (LAST_FOCUSED_ELEMENT != null && LAST_FOCUSED_ELEMENT.focus != undefined) {
setTimeout(function () {
LAST_FOCUSED_ELEMENT.focus();
}, 20);
}
LAST_FOCUSED_ELEMENT = null;
}
}
}
function announceToScreenReader(message) {
if (message == undefined || message.length == 0) {
return;
}
var region = document.getElementById("sr-announcer");
if (region == null) {
return;
}
region.innerText = "";
setTimeout(function () {
region.innerText = message;
}, 20);
} }
function setConnectionState(state, text) { function setConnectionState(state, text) {
var label = text; var label = text;
@@ -75,6 +112,8 @@ function setConnectionState(state, text) {
} }
indicator.className = "status-" + state; indicator.className = "status-" + state;
indicator.innerText = label; indicator.innerText = label;
indicator.setAttribute("aria-label", "Connection status: " + label);
announceToScreenReader("Connection status " + label);
} }
function changeButtonAction(element, buttonID, attribute) { function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value; var value = element.options[element.selectedIndex].value;
@@ -190,15 +229,29 @@ function sortTable(column) {
var table = document.getElementById("content_table"); var table = document.getElementById("content_table");
var tableHead = table.getElementsByTagName("TR")[0]; var tableHead = table.getElementsByTagName("TR")[0];
var tableItems = tableHead.getElementsByTagName("TD"); var tableItems = tableHead.getElementsByTagName("TD");
for (var h = 0; h < tableItems.length; h++) {
if (tableItems[h].getAttribute("role") == "columnheader") {
tableItems[h].setAttribute("aria-sort", "none");
}
}
var sortObj = new Object(); var sortObj = new Object();
var x, xValue; var x, xValue;
var tableHeader; var tableHeader;
var sortByString = false; var sortByString = false;
if (column > 0 && COLUMN_TO_SORT > 0) { if (column > 0 && COLUMN_TO_SORT > 0) {
tableItems[COLUMN_TO_SORT].className = "pointer"; tableItems[COLUMN_TO_SORT].classList.remove("sortThis");
tableItems[column].className = "sortThis"; tableItems[COLUMN_TO_SORT].classList.add("pointer");
tableItems[column].classList.remove("pointer");
tableItems[column].classList.add("sortThis");
} }
COLUMN_TO_SORT = column; COLUMN_TO_SORT = column;
var mobileSort = document.getElementById("mapping-sort-mobile");
if (mobileSort != null && (column == 1 || column == 3 || column == 4 || column == 5)) {
mobileSort.value = column.toString();
}
if (tableItems[column] != undefined && tableItems[column].getAttribute("role") == "columnheader") {
tableItems[column].setAttribute("aria-sort", "ascending");
}
var rows = table.rows; var rows = table.rows;
if (rows[1] != undefined) { if (rows[1] != undefined) {
tableHeader = rows[0]; tableHeader = rows[0];
@@ -258,6 +311,7 @@ function createSearchObj() {
var channels = getObjKeys(data); var channels = getObjKeys(data);
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"]; var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"];
channels.forEach(function (id) { channels.forEach(function (id) {
SEARCH_MAPPING[id] = "";
channelKeys.forEach(function (key) { channelKeys.forEach(function (key) {
if (key == "x-active") { if (key == "x-active") {
switch (data[id][key]) { switch (data[id][key]) {
@@ -290,6 +344,9 @@ function searchInMapping() {
for (var i = 1; i < trs.length; ++i) { for (var i = 1; i < trs.length; ++i) {
var id = trs[i].getAttribute("id"); var id = trs[i].getAttribute("id");
var element = SEARCH_MAPPING[id]; var element = SEARCH_MAPPING[id];
if (element == undefined) {
continue;
}
switch (element.toLowerCase().includes(searchValue.toLowerCase())) { switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
case true: case true:
document.getElementById(id).style.display = ""; document.getElementById(id).style.display = "";
@@ -299,6 +356,7 @@ function searchInMapping() {
break; break;
} }
} }
announceToScreenReader("Search updated");
return; return;
} }
function calculateWrapperHeight() { function calculateWrapperHeight() {

View File

@@ -35,7 +35,9 @@ var WizardItem = /** @class */ (function (_super) {
var key = this.key; var key = this.key;
var content = new PopupContent(); var content = new PopupContent();
var description; var description;
var wizardField = null;
var doc = document.getElementById(this.DocumentID); var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = ""; doc.innerHTML = "";
doc.appendChild(headline); doc.appendChild(headline);
switch (key) { switch (key) {
@@ -50,6 +52,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard"); select.setAttribute("class", "wizard");
select.id = key; select.id = key;
doc.appendChild(select); doc.appendChild(select);
wizardField = select;
description = "{{.wizard.tuner.description}}"; description = "{{.wizard.tuner.description}}";
break; break;
case "epgSource": case "epgSource":
@@ -59,6 +62,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard"); select.setAttribute("class", "wizard");
select.id = key; select.id = key;
doc.appendChild(select); doc.appendChild(select);
wizardField = select;
description = "{{.wizard.epgSource.description}}"; description = "{{.wizard.epgSource.description}}";
break; break;
case "m3u": case "m3u":
@@ -67,6 +71,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard"); input.setAttribute("class", "wizard");
input.id = key; input.id = key;
doc.appendChild(input); doc.appendChild(input);
wizardField = input;
description = "{{.wizard.m3u.description}}"; description = "{{.wizard.m3u.description}}";
break; break;
case "xmltv": case "xmltv":
@@ -75,6 +80,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard"); input.setAttribute("class", "wizard");
input.id = key; input.id = key;
doc.appendChild(input); doc.appendChild(input);
wizardField = input;
description = "{{.wizard.xmltv.description}}"; description = "{{.wizard.xmltv.description}}";
break; break;
default: default:
@@ -82,8 +88,20 @@ var WizardItem = /** @class */ (function (_super) {
break; break;
} }
var pre = document.createElement("PRE"); var pre = document.createElement("PRE");
pre.id = "wizard-description-" + key;
pre.innerHTML = description; pre.innerHTML = description;
doc.appendChild(pre); doc.appendChild(pre);
if (wizardField != null) {
wizardField.setAttribute("aria-label", this.headline);
wizardField.setAttribute("aria-describedby", pre.id);
setTimeout(function () {
wizardField.focus();
}, 20);
}
doc.setAttribute("aria-busy", "false");
if (typeof announceToScreenReader == "function") {
announceToScreenReader(this.headline + " step");
}
console.log(headline, key); console.log(headline, key);
}; };
return WizardItem; return WizardItem;
@@ -145,3 +163,20 @@ configurationWizard.push(new WizardItem("tuner", "{{.wizard.tuner.title}}"));
configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}")); configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}"));
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}")); configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"));
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}")); configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}"));
document.addEventListener("DOMContentLoaded", function () {
var container = document.getElementById("content");
if (container == null) {
return;
}
container.addEventListener("keydown", function (event) {
if (event.key != "Enter") {
return;
}
var target = event.target;
if (target == null || target.tagName != "INPUT") {
return;
}
event.preventDefault();
saveWizard();
});
});

View File

@@ -44,7 +44,13 @@ var MainMenuItem = /** @class */ (function (_super) {
item.setAttribute("onclick", "javascript: openThisMenu(this)"); item.setAttribute("onclick", "javascript: openThisMenu(this)");
item.setAttribute("id", this.id); item.setAttribute("id", this.id);
item.setAttribute("data-menu", this.menuKey); item.setAttribute("data-menu", this.menuKey);
item.setAttribute("role", "menuitem");
item.setAttribute("tabindex", "0");
item.setAttribute("aria-controls", "content");
item.setAttribute("aria-label", this.value);
item.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();openThisMenu(this);}");
var img = this.createIMG(this.imgSrc); var img = this.createIMG(this.imgSrc);
img.setAttribute("alt", "");
var value = this.createValue(this.value); var value = this.createValue(this.value);
item.appendChild(img); item.appendChild(img);
item.appendChild(value); item.appendChild(value);
@@ -452,7 +458,7 @@ var Cell = /** @class */ (function () {
break; break;
case "INPUTCHANNEL": case "INPUTCHANNEL":
element = document.createElement("INPUT"); element = document.createElement("INPUT");
element.setAttribute("onchange", "javscript: changeChannelNumber(this)"); element.setAttribute("onchange", "javascript: changeChannelNumber(this)");
element.value = this.value; element.value = this.value;
element.type = "text"; element.type = "text";
break; break;
@@ -484,10 +490,18 @@ var Cell = /** @class */ (function () {
} }
if (this.onclick == true) { if (this.onclick == true) {
td.setAttribute("onclick", this.onclickFunktion); td.setAttribute("onclick", this.onclickFunktion);
td.className = "pointer"; td.className = "pointer keyboard-clickable";
td.setAttribute("tabindex", "0");
td.setAttribute("role", "button");
td.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();this.click();}");
} }
if (this.tdClassName != undefined) { if (this.tdClassName != undefined) {
td.className = this.tdClassName; if (td.className.length > 0) {
td.className = td.className + " " + this.tdClassName;
}
else {
td.className = this.tdClassName;
}
} }
return td; return td;
}; };
@@ -511,6 +525,7 @@ var ShowContent = /** @class */ (function (_super) {
COLUMN_TO_SORT = -1; COLUMN_TO_SORT = -1;
// Alten Inhalt löschen // Alten Inhalt löschen
var doc = document.getElementById(this.DocumentID); var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = ""; doc.innerHTML = "";
showPreview(false); showPreview(false);
// Überschrift // Überschrift
@@ -557,9 +572,31 @@ var ShowContent = /** @class */ (function (_super) {
var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}"); var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}");
input.setAttribute("onclick", 'javascript: bulkEdit()'); input.setAttribute("onclick", 'javascript: bulkEdit()');
interaction.appendChild(input); interaction.appendChild(input);
var sortSelect = document.createElement("SELECT");
sortSelect.setAttribute("id", "mapping-sort-mobile");
sortSelect.className = "mobile-only-control";
sortSelect.setAttribute("aria-label", "Sort mapping");
var sortOptions = [
{ label: "{{.mapping.table.chNo}}", value: "1" },
{ label: "{{.mapping.table.channelName}}", value: "3" },
{ label: "{{.mapping.table.playlist}}", value: "4" },
{ label: "{{.mapping.table.groupTitle}}", value: "5" }
];
sortOptions.forEach(function (optionData) {
var option = document.createElement("OPTION");
option.innerText = optionData.label;
option.value = optionData.value;
sortSelect.appendChild(option);
});
sortSelect.value = "1";
sortSelect.onchange = function () {
sortTable(parseInt(this.value, 10));
};
interaction.appendChild(sortSelect);
var input = this.createInput("search", "search", ""); var input = this.createInput("search", "search", "");
input.setAttribute("id", "searchMapping"); input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "{{.button.search}}"); input.setAttribute("placeholder", "{{.button.search}}");
input.setAttribute("aria-label", "{{.button.search}}");
input.className = "search"; input.className = "search";
input.setAttribute("oninput", 'javascript: searchInMapping()'); input.setAttribute("oninput", 'javascript: searchInMapping()');
interaction.appendChild(input); interaction.appendChild(input);
@@ -581,6 +618,7 @@ var ShowContent = /** @class */ (function (_super) {
var settings = this.createDIV(); var settings = this.createDIV();
wrapper.appendChild(settings); wrapper.appendChild(settings);
showSettings(); showSettings();
finalizeContentAccessibility(headline);
return; return;
break; break;
case "log": case "log":
@@ -594,6 +632,7 @@ var ShowContent = /** @class */ (function (_super) {
var logs = this.createDIV(); var logs = this.createDIV();
wrapper.appendChild(logs); wrapper.appendChild(logs);
showLogs(true); showLogs(true);
finalizeContentAccessibility(headline);
return; return;
break; break;
case "logout": case "logout":
@@ -666,10 +705,129 @@ var ShowContent = /** @class */ (function (_super) {
break; break;
} }
showElement("loading", false); showElement("loading", false);
finalizeContentAccessibility(headline);
}; };
return ShowContent; return ShowContent;
}(Content)); }(Content));
var SHELL_LAYOUT_READY = false; var SHELL_LAYOUT_READY = false;
function isKeyboardActivationKey(event) {
return event.key == "Enter" || event.key == " ";
}
function makeKeyboardClickable(element, label) {
if (element == null) {
return;
}
if (element.getAttribute("data-keyboard-ready") == "true") {
return;
}
var tagName = element.tagName.toUpperCase();
if (tagName == "INPUT" || tagName == "BUTTON" || tagName == "SELECT" || tagName == "TEXTAREA" || tagName == "A") {
return;
}
element.setAttribute("data-keyboard-ready", "true");
if (element.getAttribute("tabindex") == null) {
element.setAttribute("tabindex", "0");
}
if (element.getAttribute("role") == null) {
element.setAttribute("role", "button");
}
element.classList.add("keyboard-clickable");
if (label != undefined && label.length > 0) {
if (element.getAttribute("aria-label") == null || element.getAttribute("aria-label").length == 0) {
element.setAttribute("aria-label", label);
}
}
element.addEventListener("keydown", function (event) {
if (isKeyboardActivationKey(event) == false) {
return;
}
event.preventDefault();
this.click();
});
}
function applyTableAccessibility(table, sectionName) {
if (table == null) {
return;
}
table.setAttribute("role", "table");
var rows = table.getElementsByTagName("TR");
var headerLabels = [];
if (rows.length > 0) {
var headerCells = rows[0].getElementsByTagName("TD");
for (var h = 0; h < headerCells.length; h++) {
var headerLabel = headerCells[h].innerText;
if (headerLabel == undefined || headerLabel.length == 0) {
headerLabel = "Value";
}
if (headerLabel == "BULK") {
headerLabel = "Select";
}
headerLabels.push(headerLabel);
}
}
for (var i = 0; i < rows.length; i++) {
rows[i].setAttribute("role", "row");
if (rows[i].getAttribute("onclick") != null) {
var rowLabel = rows[i].innerText;
if (rowLabel == undefined || rowLabel.length == 0) {
rowLabel = sectionName + " row";
}
makeKeyboardClickable(rows[i], rowLabel);
}
var cells = rows[i].getElementsByTagName("TD");
for (var c = 0; c < cells.length; c++) {
if (i == 0) {
cells[c].setAttribute("role", "columnheader");
}
else {
cells[c].setAttribute("role", "cell");
}
var dataLabel = headerLabels[c];
if (dataLabel == undefined || dataLabel.length == 0) {
dataLabel = "Value";
}
cells[c].setAttribute("data-label", dataLabel);
var checkbox = cells[c].querySelector('input[type="checkbox"]');
if (checkbox != null) {
cells[c].setAttribute("data-cell-type", "checkbox");
}
else {
var image = cells[c].querySelector("img");
if (image != null) {
cells[c].setAttribute("data-cell-type", "image");
}
else {
cells[c].removeAttribute("data-cell-type");
}
}
if (cells[c].getAttribute("onclick") != null) {
var cellLabel = cells[c].innerText;
if (cellLabel == undefined || cellLabel.length == 0) {
cellLabel = sectionName + " details";
}
makeKeyboardClickable(cells[c], cellLabel);
}
}
}
}
function finalizeContentAccessibility(sectionName) {
var content = document.getElementById("content");
if (content == null) {
return;
}
content.setAttribute("aria-busy", "false");
var heading = content.getElementsByTagName("H3")[0];
if (heading != null) {
heading.setAttribute("tabindex", "-1");
setTimeout(function () {
heading.focus();
}, 20);
}
applyTableAccessibility(document.getElementById("content_table"), sectionName);
if (sectionName != undefined && sectionName.length > 0) {
announceToScreenReader(sectionName + " loaded");
}
}
function setLayoutMenuState(open) { function setLayoutMenuState(open) {
if (document.body == null) { if (document.body == null) {
return; return;
@@ -680,6 +838,35 @@ function setLayoutMenuState(open) {
else { else {
document.body.classList.remove("menu-open"); document.body.classList.remove("menu-open");
} }
var toggle = document.getElementById("menu-toggle");
if (toggle != null) {
toggle.setAttribute("aria-expanded", open == true ? "true" : "false");
toggle.setAttribute("aria-label", open == true ? "Close navigation menu" : "Open navigation menu");
}
var overlay = document.getElementById("layout-overlay");
if (overlay != null) {
overlay.setAttribute("aria-hidden", open == true ? "false" : "true");
}
var wrapper = document.getElementById("menu-wrapper");
if (wrapper != null) {
if (window.innerWidth <= 900) {
wrapper.setAttribute("aria-hidden", open == true ? "false" : "true");
}
else {
wrapper.setAttribute("aria-hidden", "false");
}
}
if (window.innerWidth <= 900 && open == false && toggle != null && wrapper != null && wrapper.contains(document.activeElement)) {
toggle.focus();
}
if (window.innerWidth <= 900 && open == true && wrapper != null) {
var firstMenuItem = wrapper.querySelector("#main-menu li");
if (firstMenuItem != null) {
setTimeout(function () {
firstMenuItem.focus();
}, 30);
}
}
} }
function toggleLayoutMenu() { function toggleLayoutMenu() {
if (document.body == null) { if (document.body == null) {
@@ -702,10 +889,12 @@ function setActiveMenu(menuID) {
var items = menu.getElementsByTagName("LI"); var items = menu.getElementsByTagName("LI");
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active"); items[i].classList.remove("menu-active");
items[i].removeAttribute("aria-current");
} }
var activeItem = document.getElementById(ACTIVE_MENU_ID); var activeItem = document.getElementById(ACTIVE_MENU_ID);
if (activeItem != null) { if (activeItem != null) {
activeItem.classList.add("menu-active"); activeItem.classList.add("menu-active");
activeItem.setAttribute("aria-current", "page");
} }
} }
function renderStatusCards() { function renderStatusCards() {
@@ -728,6 +917,7 @@ function renderStatusCards() {
cards.forEach(function (card) { cards.forEach(function (card) {
var box = document.createElement("DIV"); var box = document.createElement("DIV");
box.className = "status-card status-card-" + card.tone; box.className = "status-card status-card-" + card.tone;
box.setAttribute("role", "listitem");
var label = document.createElement("P"); var label = document.createElement("P");
label.className = "status-card-label"; label.className = "status-card-label";
label.innerText = card.label; label.innerText = card.label;
@@ -741,6 +931,7 @@ function renderStatusCards() {
} }
box.appendChild(label); box.appendChild(label);
box.appendChild(value); box.appendChild(value);
box.setAttribute("aria-label", card.label + ": " + value.innerText);
wrapper.appendChild(box); wrapper.appendChild(box);
}); });
} }
@@ -768,7 +959,8 @@ function initShellLayout() {
} }
if (event.key == "/") { if (event.key == "/") {
var target = event.target; var target = event.target;
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT"; var tagName = target != null && target.tagName != undefined ? target.tagName : "";
var onInput = tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
if (onInput == true) { if (onInput == true) {
return; return;
} }
@@ -779,6 +971,7 @@ function initShellLayout() {
} }
} }
}); });
setLayoutMenuState(false);
setConnectionState("idle"); setConnectionState("idle");
SHELL_LAYOUT_READY = true; SHELL_LAYOUT_READY = true;
} }
@@ -798,6 +991,10 @@ function PageReady() {
return; return;
} }
function createLayout() { function createLayout() {
var contentRegion = document.getElementById("content");
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "true");
}
// Client Info // Client Info
var obj = SERVER["clientInfo"]; var obj = SERVER["clientInfo"];
var keys = getObjKeys(obj); var keys = getObjKeys(obj);
@@ -808,6 +1005,9 @@ function createLayout() {
} }
renderStatusCards(); renderStatusCards();
if (!document.getElementById("main-menu")) { if (!document.getElementById("main-menu")) {
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "false");
}
return; return;
} }
// Menü erstellen // Menü erstellen
@@ -835,6 +1035,7 @@ function createLayout() {
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) { if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID); setActiveMenu(ACTIVE_MENU_ID);
} }
setLayoutMenuState(document.body.classList.contains("menu-open"));
var content = document.getElementById("content"); var content = document.getElementById("content");
var menu = document.getElementById("main-menu"); var menu = document.getElementById("main-menu");
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) { if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
@@ -845,6 +1046,9 @@ function createLayout() {
} }
} }
} }
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "false");
}
return; return;
} }
function openThisMenu(element) { function openThisMenu(element) {
@@ -852,6 +1056,10 @@ function openThisMenu(element) {
var content = new ShowContent(id); var content = new ShowContent(id);
setActiveMenu(id); setActiveMenu(id);
content.show(); content.show();
var contentArea = document.getElementById("content");
if (contentArea != null) {
contentArea.scrollTop = 0;
}
closeLayoutMenuIfMobile(); closeLayoutMenuIfMobile();
calculateWrapperHeight(); calculateWrapperHeight();
return; return;
@@ -890,9 +1098,26 @@ var PopupContent = /** @class */ (function (_super) {
} }
PopupContent.prototype.createHeadline = function (headline) { PopupContent.prototype.createHeadline = function (headline) {
this.doc.innerHTML = ""; this.doc.innerHTML = "";
var titleBar = document.createElement("DIV");
titleBar.className = "popup-title";
var element = document.createElement("H3"); var element = document.createElement("H3");
element.id = "popup-title-text";
element.innerHTML = headline.toUpperCase(); element.innerHTML = headline.toUpperCase();
this.doc.appendChild(element); titleBar.appendChild(element);
var closeButton = document.createElement("BUTTON");
closeButton.setAttribute("type", "button");
closeButton.className = "popup-close";
closeButton.setAttribute("aria-label", "Close dialog");
closeButton.innerHTML = "&times;";
closeButton.onclick = function () {
showElement("popup", false);
};
titleBar.appendChild(closeButton);
this.doc.appendChild(titleBar);
var popup = document.getElementById("popup");
if (popup != null) {
popup.setAttribute("aria-labelledby", "popup-title-text");
}
// Tabelle erstellen // Tabelle erstellen
this.table = document.createElement("TABLE"); this.table = document.createElement("TABLE");
this.doc.appendChild(this.table); this.doc.appendChild(this.table);

View File

@@ -74,6 +74,29 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft); setting.appendChild(tdLeft);
setting.appendChild(tdRight); setting.appendChild(tdRight);
break; break;
case "plex.url":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "plex.url", data);
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "plex.token":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("password", "plex.token", data);
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}");
input.setAttribute("autocomplete", "off");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "buffer.timeout": case "buffer.timeout":
var tdLeft = document.createElement("TD"); var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"; tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":";
@@ -240,6 +263,17 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft); setting.appendChild(tdLeft);
setting.appendChild(tdRight); setting.appendChild(tdRight);
break; break;
case "use_plexAPI":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
// Select // Select
case "tuner": case "tuner":
var tdLeft = document.createElement("TD"); var tdLeft = document.createElement("TD");
@@ -364,6 +398,12 @@ var SettingsCategory = /** @class */ (function () {
case "user.agent": case "user.agent":
text = "{{.settings.userAgent.description}}"; text = "{{.settings.userAgent.description}}";
break; break;
case "plex.url":
text = "{{.settings.plexURL.description}}";
break;
case "plex.token":
text = "{{.settings.plexToken.description}}";
break;
case "ffmpeg.path": case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}"; text = "{{.settings.ffmpegPath.description}}";
break; break;
@@ -388,6 +428,9 @@ var SettingsCategory = /** @class */ (function () {
case "api": case "api":
text = "{{.settings.api.description}}"; text = "{{.settings.api.description}}";
break; break;
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}";
break;
case "files.update": case "files.update":
text = "{{.settings.filesUpdate.description}}"; text = "{{.settings.filesUpdate.description}}";
break; break;

View File

@@ -355,6 +355,23 @@
"title": "API Interface", "title": "API Interface",
"description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>" "description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
}, },
"usePlexAPI":
{
"title": "Use Plex API Refresh",
"description": "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."
},
"plexURL":
{
"title": "Plex Server URL",
"description": "Base URL of your Plex server. Example: http://192.168.1.10:32400",
"placeholder": "http://plex-host:32400"
},
"plexToken":
{
"title": "Plex API Token",
"description": "Plex token used for authenticated API calls to refresh your DVR guide.",
"placeholder": "Plex X-Plex-Token"
},
"epgSource": "epgSource":
{ {
"title": "EPG Source", "title": "EPG Source",
@@ -545,4 +562,4 @@
"placeholder": "Confirm" "placeholder": "Confirm"
} }
} }
} }

View File

@@ -14,32 +14,32 @@
<div id="header" class="imgCenter"></div> <div id="header" class="imgCenter"></div>
<div id="box"> <main id="box" role="main" aria-labelledby="head-text">
<div id="headline"> <div id="headline">
<h1 id="head-text" class="center">{{.login.headline}}</h1> <h1 id="head-text" class="center">{{.login.headline}}</h1>
</div> </div>
<p id="err" class="errorMsg center">{{.authenticationErr}}</p> <p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true">{{.authenticationErr}}</p>
<div id="content"> <div id="content">
<form id="authentication" action="" method="post"> <form id="authentication" action="" method="post" aria-describedby="err" novalidate>
<h5>{{.login.username.title}}:</h5> <label for="username">{{.login.username.title}}:</label>
<input id="username" type="text" name="username" placeholder="Username" value=""> <input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
<h5>{{.login.password.title}}:</h5> <label for="password">{{.login.password.title}}:</label>
<input id="password" type="password" name="password" placeholder="Password" value=""> <input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="current-password">
</form> </form>
</div> </div>
<div id="box-footer"> <div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.login}}" onclick="javascript: login();"> <input id="submit" class="" type="button" value="{{.button.login}}" aria-label="{{.button.login}}" onclick="javascript: login();">
</div> </div>
</div> </main>
</body> </body>

View File

@@ -23,6 +23,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
var reloadData = false var reloadData = false
var cacheImages = false var cacheImages = false
var createXEPGFiles = false var createXEPGFiles = false
var triggerPlexGuideReload = false
var debug string var debug string
// -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}' // -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}'
@@ -36,6 +37,21 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
case "tuner": case "tuner":
showWarning(2105) showWarning(2105)
case "use_plexAPI":
triggerPlexGuideReload = true
case "plex.url":
if v, ok := value.(string); ok {
value = strings.TrimRight(strings.TrimSpace(v), "/")
}
triggerPlexGuideReload = true
case "plex.token":
if v, ok := value.(string); ok {
value = strings.TrimSpace(v)
}
triggerPlexGuideReload = true
case "epgSource": case "epgSource":
reloadData = true reloadData = true
@@ -119,22 +135,26 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
oldSettings[key] = value oldSettings[key] = value
switch fmt.Sprintf("%T", value) { if key == "plex.token" {
debug = fmt.Sprintf("Save Setting:Key: %s | Value: ******** (%T)", key, value)
} else {
switch fmt.Sprintf("%T", value) {
case "bool": case "bool":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value) debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
case "string": case "string":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value) debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
case "[]interface {}": case "[]interface {}":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value) debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
case "float64": case "float64":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value) debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
default: default:
debug = fmt.Sprintf("%T", value) debug = fmt.Sprintf("%T", value)
}
} }
showDebug(debug, 1) showDebug(debug, 1)
@@ -250,6 +270,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
} }
if triggerPlexGuideReload == true {
queuePlexGuideRefresh("settings change")
}
} }
return return
@@ -980,6 +1004,10 @@ func buildDatabaseDVR() (err error) {
sort.Strings(Data.StreamPreviewUI.Active) sort.Strings(Data.StreamPreviewUI.Active)
sort.Strings(Data.StreamPreviewUI.Inactive) sort.Strings(Data.StreamPreviewUI.Inactive)
if Settings.EpgSource != "XEPG" {
queuePlexGuideRefresh("lineup update")
}
return return
} }

363
src/plex_api.go Normal file
View File

@@ -0,0 +1,363 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
var plexRefreshState = struct {
sync.Mutex
lastRun time.Time
scheduled bool
inProgress bool
queued bool
reason string
missingConfigLogged bool
}{}
// queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh.
func queuePlexGuideRefresh(reason string) {
if Settings.UsePlexAPI == false {
return
}
plexRefreshState.Lock()
if len(strings.TrimSpace(reason)) > 0 {
plexRefreshState.reason = reason
}
if plexRefreshState.scheduled == true || plexRefreshState.inProgress == true {
plexRefreshState.queued = true
plexRefreshState.Unlock()
return
}
plexRefreshState.scheduled = true
delay := plexRefreshDelayLocked()
plexRefreshState.Unlock()
go runPlexGuideRefresh(delay)
}
func runPlexGuideRefresh(initialDelay time.Duration) {
if initialDelay > 0 {
time.Sleep(initialDelay)
}
for {
plexRefreshState.Lock()
reason := strings.TrimSpace(plexRefreshState.reason)
if len(reason) == 0 {
reason = "update"
}
plexRefreshState.scheduled = false
plexRefreshState.inProgress = true
plexRefreshState.queued = false
plexRefreshState.Unlock()
err := refreshPlexGuide(reason)
if err != nil {
ShowError(err, 0)
}
plexRefreshState.Lock()
plexRefreshState.lastRun = time.Now()
runAgain := plexRefreshState.queued
plexRefreshState.inProgress = false
nextDelay := time.Duration(0)
if runAgain == true && Settings.UsePlexAPI == true {
plexRefreshState.scheduled = true
nextDelay = plexRefreshDelayLocked()
}
plexRefreshState.Unlock()
if runAgain == false || Settings.UsePlexAPI == false {
return
}
time.Sleep(nextDelay)
}
}
func plexRefreshDelayLocked() (delay time.Duration) {
const minDelay = 2 * time.Second
const cooldown = 20 * time.Second
if plexRefreshState.lastRun.IsZero() == true {
return minDelay
}
since := time.Since(plexRefreshState.lastRun)
if since >= cooldown {
return minDelay
}
return cooldown - since
}
func refreshPlexGuide(reason string) (err error) {
baseURL, token, ready := getPlexConfig()
if ready == false {
return nil
}
dvrs, err := discoverPlexDVRs(baseURL, token)
if err != nil {
return
}
var refreshed int
var failed int
for _, dvr := range dvrs {
endpoints := getPlexReloadEndpoints(dvr)
if len(endpoints) == 0 {
failed++
continue
}
var endpointErr error
for _, endpoint := range endpoints {
status, _, requestErr := doPlexRequest("POST", baseURL, endpoint, token)
if requestErr != nil {
endpointErr = requestErr
continue
}
if status >= 200 && status < 300 {
refreshed++
endpointErr = nil
break
}
endpointErr = fmt.Errorf("Plex API returned HTTP %d for %s", status, endpoint)
}
if endpointErr != nil {
failed++
}
}
if refreshed == 0 {
if failed == 0 {
return errors.New("Plex API guide refresh failed")
}
return fmt.Errorf("Plex API guide refresh failed for %d DVR(s)", failed)
}
showInfo(fmt.Sprintf("Plex API:Guide reload requested for %d DVR(s) (%s)", refreshed, reason))
if failed > 0 {
showInfo(fmt.Sprintf("Plex API:Guide reload failed for %d DVR(s)", failed))
}
return nil
}
func getPlexConfig() (baseURL, token string, ready bool) {
baseURL = strings.TrimSpace(Settings.PlexURL)
token = strings.TrimSpace(Settings.PlexToken)
if strings.HasPrefix(baseURL, "http://") == false && strings.HasPrefix(baseURL, "https://") == false && len(baseURL) > 0 {
baseURL = "http://" + baseURL
}
baseURL = strings.TrimRight(baseURL, "/")
plexRefreshState.Lock()
defer plexRefreshState.Unlock()
if len(baseURL) == 0 || len(token) == 0 {
if plexRefreshState.missingConfigLogged == false {
showInfo("Plex API:Skipped refresh because plex.url or plex.token is empty")
plexRefreshState.missingConfigLogged = true
}
return "", "", false
}
plexRefreshState.missingConfigLogged = false
return baseURL, token, true
}
func discoverPlexDVRs(baseURL, token string) (dvrs []map[string]interface{}, err error) {
status, body, err := doPlexRequest("GET", baseURL, "/livetv/dvrs", token)
if err != nil {
return
}
if status < 200 || status >= 300 {
err = fmt.Errorf("Plex API returned HTTP %d for /livetv/dvrs", status)
return
}
var payload = make(map[string]interface{})
err = json.Unmarshal(body, &payload)
if err != nil {
return
}
mediaContainer, ok := payload["MediaContainer"].(map[string]interface{})
if ok == false {
err = errors.New("Plex API response missing MediaContainer")
return
}
for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} {
if raw, found := mediaContainer[key]; found == true {
if list, ok := raw.([]interface{}); ok == true {
dvrs = convertToPlexMapSlice(list)
if len(dvrs) > 0 {
return
}
}
}
}
err = errors.New("Plex API returned no DVR entries")
return
}
func convertToPlexMapSlice(list []interface{}) (dvrs []map[string]interface{}) {
dvrs = make([]map[string]interface{}, 0, len(list))
for _, item := range list {
if dvr, ok := item.(map[string]interface{}); ok == true {
dvrs = append(dvrs, dvr)
}
}
return
}
func getPlexReloadEndpoints(dvr map[string]interface{}) (endpoints []string) {
endpoints = make([]string, 0, 6)
added := make(map[string]bool)
add := func(endpoint string) {
endpoint = strings.TrimSpace(endpoint)
if len(endpoint) == 0 {
return
}
if strings.HasPrefix(endpoint, "/") == false {
endpoint = "/" + endpoint
}
if added[endpoint] == true {
return
}
added[endpoint] = true
endpoints = append(endpoints, endpoint)
}
if key := getPlexMapString(dvr, "key"); len(key) > 0 {
if strings.HasPrefix(key, "/") == true {
add(strings.TrimRight(key, "/") + "/reloadGuide")
} else {
add("/livetv/dvrs/" + url.PathEscape(key) + "/reloadGuide")
}
}
for _, field := range []string{"uuid", "identifier", "machineIdentifier", "clientIdentifier", "id"} {
if value := getPlexMapString(dvr, field); len(value) > 0 {
add("/livetv/dvrs/" + url.PathEscape(value) + "/reloadGuide")
}
}
return
}
func getPlexMapString(data map[string]interface{}, field string) string {
for key, value := range data {
if strings.EqualFold(key, field) == false {
continue
}
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
case float64:
return strconv.FormatInt(int64(v), 10)
}
}
return ""
}
func doPlexRequest(method, baseURL, endpoint, token string) (status int, body []byte, err error) {
requestURL := strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")
req, err := http.NewRequest(method, requestURL, nil)
if err != nil {
return
}
query := req.URL.Query()
if len(token) > 0 {
query.Set("X-Plex-Token", token)
req.URL.RawQuery = query.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Plex-Token", token)
if len(System.DeviceID) > 0 {
req.Header.Set("X-Plex-Client-Identifier", System.DeviceID)
}
if len(System.Name) > 0 {
req.Header.Set("X-Plex-Product", System.Name)
req.Header.Set("X-Plex-Device-Name", System.Name)
}
if len(System.Version) > 0 {
req.Header.Set("X-Plex-Version", System.Version)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
status = resp.StatusCode
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
return
}

View File

@@ -254,6 +254,9 @@ type Notification struct {
// SettingsStruct : Inhalt der settings.json // SettingsStruct : Inhalt der settings.json
type SettingsStruct struct { type SettingsStruct struct {
API bool `json:"api"` API bool `json:"api"`
UsePlexAPI bool `json:"use_plexAPI"`
PlexURL string `json:"plex.url"`
PlexToken string `json:"plex.token"`
AuthenticationAPI bool `json:"authentication.api"` AuthenticationAPI bool `json:"authentication.api"`
AuthenticationM3U bool `json:"authentication.m3u"` AuthenticationM3U bool `json:"authentication.m3u"`
AuthenticationPMS bool `json:"authentication.pms"` AuthenticationPMS bool `json:"authentication.pms"`

View File

@@ -18,6 +18,9 @@ type RequestStruct struct {
// Neue Werte für die Einstellungen (settings.json) // Neue Werte für die Einstellungen (settings.json)
Settings struct { Settings struct {
API *bool `json:"api,omitempty"` API *bool `json:"api,omitempty"`
UsePlexAPI *bool `json:"use_plexAPI,omitempty"`
PlexURL *string `json:"plex.url,omitempty"`
PlexToken *string `json:"plex.token,omitempty"`
AuthenticationAPI *bool `json:"authentication.api,omitempty"` AuthenticationAPI *bool `json:"authentication.api,omitempty"`
AuthenticationM3U *bool `json:"authentication.m3u,omitempty"` AuthenticationM3U *bool `json:"authentication.m3u,omitempty"`
AuthenticationPMS *bool `json:"authentication.pms,omitempty"` AuthenticationPMS *bool `json:"authentication.pms,omitempty"`

View File

@@ -106,6 +106,9 @@ func loadSettings() (settings SettingsStruct, err error) {
dataMap["hdhr"] = make(map[string]interface{}) dataMap["hdhr"] = make(map[string]interface{})
defaults["api"] = false defaults["api"] = false
defaults["use_plexAPI"] = false
defaults["plex.url"] = ""
defaults["plex.token"] = ""
defaults["authentication.api"] = false defaults["authentication.api"] = false
defaults["authentication.m3u"] = false defaults["authentication.m3u"] = false
defaults["authentication.pms"] = false defaults["authentication.pms"] = false

View File

@@ -68,6 +68,7 @@ func buildXEPG(background bool) {
cleanupXEPG() cleanupXEPG()
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
@@ -84,6 +85,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0 System.ImageCachingInProgress = 0
@@ -113,6 +115,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
@@ -127,6 +130,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0 System.ImageCachingInProgress = 0
@@ -179,6 +183,7 @@ func updateXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg update")
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0 System.ScanInProgress = 0

View File

@@ -22,7 +22,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen // Kategorien für die Einstellungen
var settingsCategory = new Array() var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))

View File

@@ -75,6 +75,35 @@ class SettingsCategory {
setting.appendChild(tdRight) setting.appendChild(tdRight)
break break
case "plex.url":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "plex.url", data)
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "plex.token":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("password", "plex.token", data)
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}")
input.setAttribute("autocomplete", "off")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "buffer.timeout": case "buffer.timeout":
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":" tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"
@@ -286,6 +315,20 @@ class SettingsCategory {
setting.appendChild(tdRight) setting.appendChild(tdRight)
break break
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
// Select // Select
case "tuner": case "tuner":
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
@@ -454,6 +497,14 @@ class SettingsCategory {
text = "{{.settings.userAgent.description}}" text = "{{.settings.userAgent.description}}"
break break
case "plex.url":
text = "{{.settings.plexURL.description}}"
break
case "plex.token":
text = "{{.settings.plexToken.description}}"
break
case "ffmpeg.path": case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}" text = "{{.settings.ffmpegPath.description}}"
break break
@@ -486,6 +537,10 @@ class SettingsCategory {
text = "{{.settings.api.description}}" text = "{{.settings.api.description}}"
break break
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}"
break
case "files.update": case "files.update":
text = "{{.settings.filesUpdate.description}}" text = "{{.settings.filesUpdate.description}}"
break break