many updates
This commit is contained in:
59
.drone.yml
Normal file
59
.drone.yml
Normal 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
|
||||||
@@ -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"> </td>
|
<td id="version" class="tdVal"> </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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 = "×";
|
||||||
|
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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
50
src/data.go
50
src/data.go
@@ -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
363
src/plex_api.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user