Redesign UI and add first-party Docker runtime support

This commit is contained in:
2026-02-11 11:04:39 +11:00
parent 0e999b85b9
commit 8cb9e43a72
22 changed files with 1730 additions and 811 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.github
.DS_Store
docker-data
README-DEV.md
changelog-beta.md
*.log
*.tmp

42
Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1.7
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags='-s -w' -o /out/xteve ./xteve.go
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S xteve \
&& adduser -S -G xteve xteve \
&& mkdir -p /xteve/config \
&& chown -R xteve:xteve /xteve
WORKDIR /xteve
COPY --from=builder /out/xteve /usr/local/bin/xteve
COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
USER xteve
EXPOSE 34400/tcp
VOLUME ["/xteve/config"]
ENV XTEVE_CONFIG=/xteve/config
ENV XTEVE_PORT=34400
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- "http://127.0.0.1:${XTEVE_PORT}/lineup_status.json" > /dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

View File

@@ -48,6 +48,59 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
---
## Project Analysis (UI + Operations)
The core architecture is strong: a Go backend with websocket-driven UI updates, filesystem-based state, and very low runtime overhead.
The weakest points are mostly operational and UX-focused:
* UI was historically utility-first and desktop-biased, with limited responsive behavior and visual hierarchy.
* Container usage was documented externally but there was no first-party Dockerfile/compose setup in this repository.
* Static web assets are generated into `src/webUI.go`, which works, but creates large diffs and a heavier edit/build cycle.
### Recommended next technical improvements
1. Replace generated `src/webUI.go` with Go `embed` for simpler static asset management and cleaner PR diffs.
2. Add CI checks (`go test ./...`, build on Linux/arm64/amd64, docker build smoke test).
3. Add a dedicated health endpoint (for example `/healthz`) to decouple health checks from HDHomeRun endpoints.
4. Add integration tests around websocket commands that mutate settings/files to reduce regression risk.
---
## Container-First Run (Included In This Repo)
### Build image
```bash
docker build -t xteve:local .
```
### Run with Docker Compose (bridge mode)
```bash
docker compose up -d
```
Compose file: `docker-compose.yml`
Persistent config volume: `./docker-data/config:/xteve/config`
### Run with Docker Compose (host networking, Linux recommended for discovery)
```bash
docker compose -f docker-compose.host.yml up -d
```
Host networking improves LAN discovery behavior (SSDP/DLNA) for Plex/Emby in many setups.
### Container environment variables
* `XTEVE_CONFIG` (default: `/xteve/config`)
* `XTEVE_PORT` (default: `34400`)
### Image details
* Multi-stage build (Go builder + minimal Alpine runtime)
* Runs as non-root user (`xteve`)
* Built-in healthcheck against `http://127.0.0.1:${XTEVE_PORT}/lineup_status.json`
---
## Downloads v2 | 64 Bit only
#### 64 Bit Intel / AMD
@@ -156,4 +209,3 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
```

13
docker-compose.host.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
xteve:
build:
context: .
dockerfile: Dockerfile
container_name: xteve
restart: unless-stopped
network_mode: host
environment:
XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400"
volumes:
- ./docker-data/config:/xteve/config

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
services:
xteve:
build:
context: .
dockerfile: Dockerfile
container_name: xteve
restart: unless-stopped
environment:
XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400"
ports:
- "34400:34400/tcp"
- "1900:1900/udp"
volumes:
- ./docker-data/config:/xteve/config

9
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
CONFIG_DIR="${XTEVE_CONFIG:-/xteve/config}"
PORT="${XTEVE_PORT:-34400}"
mkdir -p "${CONFIG_DIR}"
exec /usr/local/bin/xteve -config "${CONFIG_DIR}" -port "${PORT}" "$@"

View File

@@ -13,7 +13,7 @@
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head>
<body onload="javascript: readyForConfiguration(0);">
<body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
<div id="loading" class="block">
<div class="loader"></div>
@@ -55,4 +55,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -10,7 +10,7 @@
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
@@ -44,4 +44,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,241 +1,293 @@
:root {
--bg-0: #07111d;
--bg-1: #0c1a2b;
--bg-2: #12233a;
--panel: #102238;
--panel-soft: #0f1f33;
--line: #274462;
--line-soft: #1b334d;
--text: #e9f5ff;
--text-muted: #9db5cb;
--accent: #35d2ff;
--accent-strong: #12b9ff;
--accent-soft: rgba(53, 210, 255, 0.2);
--ok: #2fd18a;
--warn: #ffc15a;
--error: #ff6f6f;
--radius-s: 10px;
--radius-m: 14px;
--radius-l: 18px;
--shadow-1: 0 20px 45px rgba(0, 0, 0, 0.32);
--shadow-2: 0 12px 30px rgba(0, 0, 0, 0.28);
}
* {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
font-family: "Arial", sans-serif;
letter-spacing: 2px;
box-sizing: border-box;
font-family: "Space Grotesk", "Avenir Next", "Trebuchet MS", sans-serif;
letter-spacing: 0.02em;
}
/*
::-webkit-scrollbar {
display: none;
}
*/
::-webkit-scrollbar {
width: 12px;
height: 12px;
width: 11px;
height: 11px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
border-radius: 5px;
border-radius: 999px;
background: rgba(7, 17, 29, 0.65);
}
::-webkit-scrollbar-thumb {
border-radius: 5px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0,0.6);
background-color: #444;
border-radius: 999px;
border: 2px solid rgba(7, 17, 29, 0.6);
background: linear-gradient(180deg, #2d597f, #1e4060);
}
::-webkit-scrollbar-thumb:hover {
background: #333;
background: linear-gradient(180deg, #3678ac, #20537c);
}
::-webkit-scrollbar-corner {
background: transparent;
::-webkit-scrollbar-corner {
background: transparent;
}
html,
body {
min-height: 100%;
margin: 0;
color: var(--text);
font-size: 14px;
background:
radial-gradient(circle at 15% 12%, rgba(64, 132, 183, 0.22), transparent 32%),
radial-gradient(circle at 88% 2%, rgba(18, 185, 255, 0.16), transparent 30%),
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 56%, #060d17 100%);
}
body {
line-height: 1.45;
}
a {
color: #00E6FF;
color: var(--accent);
}
html, body {
color: #fff;
margin: 0px auto;
height: 100%;
font-size: 14px;
a:hover {
color: #8de7ff;
}
h1,
h2,
h3,
h4,
h5 {
margin: 0;
font-weight: 600;
letter-spacing: 0.03em;
}
h1 {
font-size: 1.72rem;
}
h2 {
font-size: 24px;
letter-spacing: 2px;
font-size: 1.35rem;
}
h3 {
font-size: 22px;
letter-spacing: 1px;
font-size: 1.2rem;
}
h4 {
font-size: 20px;
letter-spacing: 1px;
line-height: 1.5em;
font-size: 1.05rem;
}
h5 {
font-size: 16px;
letter-spacing: 1px;
line-height: 1.2em;
margin: 25px 0px 10px 0px;
font-size: 0.92rem;
margin: 8px 0;
color: var(--text-muted);
}
p {
margin: 0;
padding: 0;
}
pre {
margin: 0;
color: var(--text-muted);
font-size: 12px;
white-space: pre-wrap;
line-height: 1.55;
letter-spacing: 0.015em;
font-family: "IBM Plex Mono", "SFMono-Regular", "Consolas", monospace;
}
hr {
border: 0;
height: 1px;
background: #333;
margin: 10px 0px;
}
p {
margin: 2px;
padding: 2px 5px;
}
pre {
margin: 0px 0px 5px 0px;
font-size: 12px;
color: #ddd;
letter-spacing: 1px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
font-style: normal;
font-variant: normal;
line-height: 1.6em;
margin: 12px 0;
background: linear-gradient(90deg, transparent, var(--line), transparent);
}
label {
margin-bottom: 20px;
margin-bottom: 8px;
display: block;
}
li {
list-style-type: none;
background-color: #111;
padding: 10px 20px;
cursor: pointer;
border-left: solid 2px #111;
transition: all 0.3;
transition: all 0.25s ease;
}
li:hover {
border-color: #00E6FF
select,
input,
textarea,
button {
outline: none;
color: var(--text);
font-size: 13px;
}
input,
select {
margin: 4px 0;
}
input[type=text],
input[type=search],
input[type=password],
input[type=number],
select,
textarea {
width: 100%;
border-radius: var(--radius-s);
border: 1px solid var(--line);
background-color: rgba(10, 22, 36, 0.78);
color: var(--text);
padding: 10px 11px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
input[type=text]:focus,
input[type=search]:focus,
input[type=password]:focus,
input[type=number]:focus,
select:focus,
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(53, 210, 255, 0.17);
background-color: rgba(9, 25, 40, 0.95);
}
input[type=text]::placeholder,
input[type=search]::placeholder,
input[type=password]::placeholder,
textarea::placeholder {
color: #7e9ab4;
}
input[type=button],
input[type=submit],
button {
cursor: pointer;
width: calc(100% + 2px);
border: solid 0px #00E6FF;
border-radius: 0px;
outline: none;
border: 1px solid transparent;
border-radius: 999px;
color: #03101a;
padding: 10px 18px;
margin: 8px 8px 8px 0;
font-weight: 700;
letter-spacing: 0.03em;
background: linear-gradient(135deg, #67e8ff 0%, #12b9ff 100%);
box-shadow: 0 10px 22px rgba(18, 185, 255, 0.3);
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
input[type=button]:hover,
input[type=submit]:hover,
button:hover {
transform: translateY(-1px);
filter: brightness(1.05);
box-shadow: 0 12px 24px rgba(18, 185, 255, 0.36);
}
input[type=button]:active,
input[type=submit]:active,
button:active {
transform: translateY(0);
}
input[type=button].delete {
color: #fff;
padding: 9px 10px;
display:block;
background-color: #333;
font-size: 14px;
margin: 5px 0px 5px 0px;
background: linear-gradient(135deg, #ff7f7f 0%, #e94343 100%);
box-shadow: 0 10px 20px rgba(233, 67, 67, 0.35);
}
select:focus {
outline: none;
}
input {
-webkit-appearance: none;
margin: 5px 0px;
padding: 2.5px 10px;
outline: none;
font-size: 14px;
}
input[type=button], input[type=submit] {
cursor: pointer;
background-color: #000;
margin: 10px 10px;
padding: 10px 25px;
border: solid 0px;
border-color: #000;
border-radius: 3px;
outline: none;
color: #fff;
}
input[type=button]:focus {
outline: none;
}
input[type=button]:hover {
background-color: #00E6FF;
color: #000;
}
input[type=button]:hover.delete {
background-color: red;
color: #fff;
}
input[type=text], input[type=search], input[type=password] {
color: #fff;
width: -webkit-calc(100% - 0px);
width: -moz-calc(100% - 0px);
width: calc(100% - 0px);
outline: none;
border: solid 1px transparent;
background-color: transparent;
border-bottom-color: #555;
border-radius: 0px;
padding: 8px 10px;
}
input[type="checkbox"] {
border: solid 1px #00E6FF;
background-color: #333;
height: 25px;
width: 25px;
cursor: pointer;
/*
-webkit-appearance: checkbox;
*/
}
input[type="checkbox"]:checked {
color: #fff;
background-color: #00E6FF;
/*display: inline-block;*/
}
input[type="checkbox"]:before {
position: initial;
left: 0px;
margin-left: -4px;
content: " ";
}
input[type="checkbox"]:checked:before {
position: initial;
left: 0px;
margin-left: -3px;
content: "✓";
color: #000;
}
input[type=button].cancel {
background-color: transparent;
border-color: red;
color: #ffd8d8;
border-color: rgba(255, 111, 111, 0.45);
background: rgba(87, 22, 22, 0.45);
box-shadow: none;
}
input[type=button].save{
background-color: #111;
float: right;
input[type=button].black,
input[type=submit].black {
color: #d8ecff;
border-color: var(--line);
background: rgba(14, 32, 50, 0.85);
box-shadow: none;
}
input[type=button].black, input[type=submit].black{
background-color: #000;
border-color: #000;
}
input[type=button].center{
input[type=button].center {
margin-right: auto;
margin-left: auto;
background-color: #000;
border-color: #000;
}
input[type=button].save {
margin-left: auto;
}
input[type=checkbox] {
width: 20px;
height: 20px;
border: 1px solid var(--line);
border-radius: 6px;
background-color: rgba(10, 22, 36, 0.86);
cursor: pointer;
position: relative;
}
input[type=checkbox]:checked {
border-color: #89ebff;
background-color: #39d6ff;
}
input[type=checkbox]:before {
content: "";
}
input[type=checkbox]:checked:before {
content: "\2713";
color: #032033;
font-size: 14px;
font-weight: 800;
position: absolute;
top: -1px;
left: 3px;
}
.changed {
border-color: #8de7ff !important;
box-shadow: 0 0 0 3px rgba(53, 210, 255, 0.2) !important;
}
.notAvailable {
opacity: 0.58;
cursor: not-allowed;
border-color: rgba(255, 111, 111, 0.45) !important;
}
.pointer {
@@ -243,12 +295,11 @@ input[type=button].center{
}
.pointer:hover {
color: #00E6FF;
cursor: pointer;
color: var(--accent);
}
.sortThis {
color: #00E6FF;
color: var(--accent);
}
.w40px {
@@ -271,14 +322,9 @@ input[type=button].center{
max-width: 200px;
min-width: 100px;
width: 200px;
overflow-x: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.w300px {
max-width: 300px;
white-space: nowrap;
}
.w220px {
@@ -286,46 +332,18 @@ input[type=button].center{
cursor: alias;
}
.w300px {
max-width: 300px;
}
.footer {
font-size: 10px;
font-size: 11px;
}
.center {
text-align: center;
}
.screenLogHidden {
transform: translate(0px, -110px);
}
.borderSpace {
margin-bottom: 30px;
}
.block {
}
.none {
display: none;
}
.notVisible {
height: 0px;
display: none;
opacity: 0;
border-bottom: #000 solid 0px;
}
.visible {
opacity: 1;
display: block;
border-bottom: #444 solid 1px;
padding: 10px;
}
.floatRight {
float: right;
}
@@ -334,115 +352,311 @@ input[type=button].center{
float: left;
}
.borderSpace {
margin-bottom: 30px;
}
.block {
display: block;
}
.none {
display: none;
}
.showBulk {
display: inline-flex;
}
.hideBulk {
display: none;
}
.noBulk {
}
.notVisible {
display: none;
opacity: 0;
height: 0;
}
.visible {
display: block;
opacity: 1;
}
.menu-active {
background-color: #00E6FF;
color: #011019;
}
.menu-notActive {
}
#branch {
display: table;
margin: auto;
color: red;
}
#interaction {
margin-bottom: 100px;
text-align: center;
border-bottom: solid 0px #777;
}
.half {
display: block;
width: 45%;
}
.menu {
border: solid 1px #00E6FF;
border: 1px solid var(--accent);
}
.half {
width: 45%;
}
.screenLogHidden {
transform: translate(0, -110px);
}
.infoMsg {
color: #aaa;
color: #87b0d1;
}
.errorMsg {
color: red;
color: var(--error);
}
.warningMsg {
color: yellow;
color: var(--warn);
}
.debugMsg {
color: magenta;
color: #d687ff;
}
.News, .Movie, .Series, .Sports, .Kids {
border-left: solid 2px
.News,
.Movie,
.Series,
.Sports,
.Kids {
border-left: 3px solid transparent;
padding-left: 8px;
}
.News {
border-color: tomato
border-color: #ff8f6e;
}
.Movie {
border-color: royalblue;
border-color: #46a4ff;
}
.Series {
border-color: gold;
border-color: #ffd267;
}
.Sports {
border-color: yellowgreen;
border-color: #5ee495;
}
.Kids {
border-color: mediumpurple;
border-color: #f6a8ff;
}
/* Loading */
#loading {
left: 0px;
top: 0px;
z-index: 10000;
position: absolute;
background-color: rgba(0,0,0, 0.8);
margin: auto;
width: 100%;
height: 100%;
position: fixed;
inset: 0;
z-index: 2000;
background-color: rgba(5, 12, 20, 0.72);
backdrop-filter: blur(1.5px);
}
.loader {
border: 5px solid transparent;
width: 52px;
height: 52px;
border-radius: 50%;
border-top: 5px solid #00E6FF;
border-bottom: 5px solid #00E6FF;
width: 50px;
height: 50px;
-webkit-animation: spin 1.2s linear infinite;
animation: spin 1.2s linear infinite;
border: 4px solid transparent;
border-top-color: #58ddff;
border-right-color: #12b9ff;
animation: spin 0.9s linear infinite;
position: fixed;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
#popup {
position: fixed;
inset: 0;
background-color: rgba(3, 9, 16, 0.72);
backdrop-filter: blur(2px);
z-index: 1500;
padding: 16px;
}
#popup-custom,
#mapping-detail,
#user-detail,
#file-detail {
margin: 0 auto;
max-width: 760px;
max-height: calc(100vh - 36px);
overflow: auto;
border-radius: var(--radius-l);
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(17, 34, 56, 0.97) 0%, rgba(12, 27, 44, 0.97) 100%);
box-shadow: var(--shadow-1);
padding: 16px;
animation: popupIn 0.2s ease;
}
#popup-custom h3 {
margin-bottom: 8px;
text-align: center;
color: #d5efff;
}
#popup-custom table,
#content_settings table,
#mapping-detail-table,
#user-detail-table {
width: 100%;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0 8px;
}
#popup-custom td,
#content_settings td,
#mapping-detail-table td,
#user-detail-table td {
padding: 2px 6px;
vertical-align: middle;
}
#popup-custom td.left,
#mapping-detail-table td.left,
#user-detail-table td.left {
width: 38%;
color: var(--text-muted);
}
#popup-custom input[type=text],
#popup-custom input[type=password],
#mapping-detail input[type=text],
#content_settings input[type=text],
#content_settings input[type=password] {
width: 100%;
}
#mapping-detail img {
display: block;
max-height: 44px;
margin: 8px auto 12px;
}
#file-detail input[type=text] {
width: 100%;
}
.interaction,
#interaction {
margin-top: 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
float: right;
}
.interaction input[type=button],
.interaction input[type=submit] {
margin: 0;
min-width: 110px;
}
#popup-interaction {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
#notification {
position: fixed;
right: 12px;
top: 12px;
width: 260px;
max-height: calc(100vh - 24px);
overflow: auto;
border-radius: var(--radius-m);
border: 1px solid var(--line);
background: rgba(10, 22, 36, 0.92);
box-shadow: var(--shadow-2);
}
#notification .element {
margin: 8px;
border-radius: 10px;
border-left: 4px solid var(--ok);
background: rgba(17, 35, 56, 0.84);
padding: 8px;
}
#notification h5 {
padding: 0;
margin-bottom: 6px;
}
#notification p {
font-size: 11px;
}
.tableEllipsis {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes popupIn {
0% {
opacity: 0;
transform: translateY(7px) scale(0.99);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@media only screen and (max-width: 619px) {
h1 {
font-size: 1.46rem;
}
#popup {
padding: 10px;
}
#popup-custom,
#mapping-detail,
#user-detail,
#file-detail {
max-height: calc(100vh - 20px);
padding: 12px;
}
.interaction,
#interaction,
#popup-interaction {
justify-content: stretch;
}
.interaction input[type=button],
.interaction input[type=submit],
#popup-interaction input[type=button] {
width: 100%;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<!---
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
@@ -17,7 +15,7 @@
</head>
<body onload="javascript: PageReady();">
<body class="app-shell" onload="javascript: PageReady();">
<div id="loading" class="none">
<div class="loader"></div>
@@ -27,6 +25,7 @@
<div id="popup-custom"></div>
</div>
<div id="layout-overlay"></div>
<div id="layout">
<!--
@@ -40,13 +39,18 @@
</div>
-->
<div id="menu-wrapper" class="layout-left">
<aside id="menu-wrapper" class="layout-left">
<div id= "branch"></div>
<div id="logo"></div>
<nav id="main-menu"></nav>
</div>
</aside>
<div class="layout-right">
<main class="layout-right">
<header id="shell-header">
<button id="menu-toggle" type="button">Menu</button>
<h2 id="shell-title">xTeVe Control Panel</h2>
<p id="connection-indicator" class="status-idle">Connecting...</p>
</header>
<table id="clientInfo" class="">
@@ -87,6 +91,8 @@
</tr>
</table>
<div id="status-cards" class="dashboard-cards"></div>
<div id="myStreamsBox" class="notVisible">
@@ -99,10 +105,10 @@
<div id="content" class=""></div>
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -12,6 +12,9 @@ function login() {
inputs[i].style.borderColor = "red";
err = true;
}
else {
inputs[i].style.borderColor = "";
}
data[key] = value;
}
if (err == true) {
@@ -20,7 +23,6 @@ function login() {
}
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
alert("sdafsd");
document.getElementById('password').style.borderColor = "red";
document.getElementById('confirm').style.borderColor = "red";
document.getElementById("err").innerHTML = "{{.account.failed}}";

View File

@@ -5,6 +5,7 @@ var SEARCH_MAPPING = new Object();
var UNDO = new Object();
var SERVER_CONNECTION = false;
var WS_AVAILABLE = false;
var ACTIVE_MENU_ID = "";
// Menü
var menuItems = new Array();
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
@@ -44,7 +45,36 @@ function showElement(elmID, type) {
cssClass = "none";
break;
}
document.getElementById(elmID).className = cssClass;
var element = document.getElementById(elmID);
if (element == null) {
return;
}
element.className = cssClass;
}
function setConnectionState(state, text) {
var label = text;
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected";
break;
case "busy":
label = "Syncing";
break;
case "offline":
label = "Offline";
break;
default:
label = "Connecting";
break;
}
}
var indicator = document.getElementById("connection-indicator");
if (indicator == null) {
return;
}
indicator.className = "status-" + state;
indicator.innerText = label;
}
function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value;
@@ -272,14 +302,15 @@ function searchInMapping() {
return;
}
function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")) {
var elm = document.getElementById("box-wrapper");
var divs = new Array("myStreamsBox", "clientInfo", "content");
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) {
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
if (elm != null && content != null) {
var contentTop = content.getBoundingClientRect().top;
var freeSpace = window.innerHeight - contentTop - 26;
if (freeSpace < 180) {
freeSpace = 180;
}
elm.style.height = window.innerHeight - elementsHeight + "px";
elm.style.height = freeSpace + "px";
}
return;
}

View File

@@ -43,6 +43,7 @@ var MainMenuItem = /** @class */ (function (_super) {
var item = document.createElement("LI");
item.setAttribute("onclick", "javascript: openThisMenu(this)");
item.setAttribute("id", this.id);
item.setAttribute("data-menu", this.menuKey);
var img = this.createIMG(this.imgSrc);
var value = this.createValue(this.value);
item.appendChild(img);
@@ -560,7 +561,7 @@ var ShowContent = /** @class */ (function (_super) {
input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "{{.button.search}}");
input.className = "search";
input.setAttribute("onchange", 'javascript: searchInMapping()');
input.setAttribute("oninput", 'javascript: searchInMapping()');
interaction.appendChild(input);
break;
case "settings":
@@ -668,10 +669,127 @@ var ShowContent = /** @class */ (function (_super) {
};
return ShowContent;
}(Content));
var SHELL_LAYOUT_READY = false;
function setLayoutMenuState(open) {
if (document.body == null) {
return;
}
if (open == true) {
document.body.classList.add("menu-open");
}
else {
document.body.classList.remove("menu-open");
}
}
function toggleLayoutMenu() {
if (document.body == null) {
return;
}
var isOpen = document.body.classList.contains("menu-open");
setLayoutMenuState(!isOpen);
}
function closeLayoutMenuIfMobile() {
if (window.innerWidth <= 900) {
setLayoutMenuState(false);
}
}
function setActiveMenu(menuID) {
ACTIVE_MENU_ID = menuID.toString();
var menu = document.getElementById("main-menu");
if (menu == null) {
return;
}
var items = menu.getElementsByTagName("LI");
for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active");
}
var activeItem = document.getElementById(ACTIVE_MENU_ID);
if (activeItem != null) {
activeItem.classList.add("menu-active");
}
}
function renderStatusCards() {
var wrapper = document.getElementById("status-cards");
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
return;
}
var info = SERVER["clientInfo"];
var errors = parseInt(info["errors"], 10);
var warnings = parseInt(info["warnings"], 10);
var cards = [
{ label: "Streams", value: info["streams"], tone: "ok" },
{ label: "EPG Source", value: info["epgSource"], tone: "neutral" },
{ label: "XEPG Channels", value: info["xepg"], tone: "ok" },
{ label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok" },
{ label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok" },
{ label: "DVR", value: info["DVR"], tone: "neutral" }
];
wrapper.innerHTML = "";
cards.forEach(function (card) {
var box = document.createElement("DIV");
box.className = "status-card status-card-" + card.tone;
var label = document.createElement("P");
label.className = "status-card-label";
label.innerText = card.label;
var value = document.createElement("P");
value.className = "status-card-value";
if (card.value == undefined || card.value == "") {
value.innerText = "-";
}
else {
value.innerText = card.value;
}
box.appendChild(label);
box.appendChild(value);
wrapper.appendChild(box);
});
}
function initShellLayout() {
if (SHELL_LAYOUT_READY == true) {
return;
}
var toggle = document.getElementById("menu-toggle");
if (toggle != null) {
toggle.onclick = function () {
toggleLayoutMenu();
};
}
var overlay = document.getElementById("layout-overlay");
if (overlay != null) {
overlay.onclick = function () {
setLayoutMenuState(false);
};
}
document.addEventListener("keydown", function (event) {
if (event.key == "Escape") {
setLayoutMenuState(false);
showElement("popup", false);
return;
}
if (event.key == "/") {
var target = event.target;
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT";
if (onInput == true) {
return;
}
var search = document.getElementById("searchMapping");
if (search != null) {
event.preventDefault();
search.focus();
}
}
});
setConnectionState("idle");
SHELL_LAYOUT_READY = true;
}
function PageReady() {
initShellLayout();
var server = new Server("getServerConfig");
server.request(new Object());
window.addEventListener("resize", function () {
if (window.innerWidth > 900) {
setLayoutMenuState(false);
}
calculateWrapperHeight();
}, true);
setInterval(function () {
@@ -688,6 +806,7 @@ function createLayout() {
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
}
renderStatusCards();
if (!document.getElementById("main-menu")) {
return;
}
@@ -713,12 +832,27 @@ function createLayout() {
break;
}
}
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID);
}
var content = document.getElementById("content");
var menu = document.getElementById("main-menu");
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
var firstItem = menu.getElementsByTagName("LI")[0];
if (firstItem != undefined) {
firstItem.click();
}
}
}
return;
}
function openThisMenu(element) {
var id = element.id;
var content = new ShowContent(id);
setActiveMenu(id);
content.show();
closeLayoutMenuIfMobile();
calculateWrapperHeight();
return;
}

View File

@@ -11,6 +11,7 @@ var Server = /** @class */ (function () {
if (this.cmd != "updateLog") {
showElement("loading", true);
UNDO = new Object();
setConnectionState("busy");
}
switch (window.location.protocol) {
case "http:":
@@ -25,6 +26,9 @@ var Server = /** @class */ (function () {
var ws = new WebSocket(url);
ws.onopen = function () {
WS_AVAILABLE = true;
if (data["cmd"] != "updateLog") {
setConnectionState("busy");
}
console.log("REQUEST (JS):");
console.log(data);
console.log("REQUEST: (JSON)");
@@ -34,6 +38,7 @@ var Server = /** @class */ (function () {
ws.onerror = function (e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
SERVER_CONNECTION = false;
setConnectionState("offline");
if (WS_AVAILABLE == false) {
alert("No websocket connection to xTeVe could be established. Check your network configuration.");
}
@@ -41,6 +46,9 @@ var Server = /** @class */ (function () {
ws.onmessage = function (e) {
SERVER_CONNECTION = false;
showElement("loading", false);
if (data["cmd"] != "updateLog") {
setConnectionState("online");
}
console.log("RESPONSE:");
var response = JSON.parse(e.data);
console.log(response);
@@ -48,6 +56,7 @@ var Server = /** @class */ (function () {
document.cookie = "Token=" + response["token"];
}
if (response["status"] == false) {
setConnectionState("offline");
alert(response["err"]);
if (response.hasOwnProperty("reload")) {
location.reload();

View File

@@ -10,7 +10,7 @@
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
@@ -43,4 +43,4 @@
</body>
</html>
</html>

View File

@@ -8,7 +8,7 @@
<link rel="stylesheet" href="css/base.css" type="text/css">
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
@@ -27,4 +27,4 @@
</div>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,8 @@ function login() {
if (value.length == 0) {
inputs[i].style.borderColor = "red"
err = true
} else {
inputs[i].style.borderColor = ""
}
data[key] = value
@@ -30,7 +32,6 @@ function login() {
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
alert("sdafsd")
document.getElementById('password').style.borderColor = "red"
document.getElementById('confirm').style.borderColor = "red"
@@ -44,4 +45,4 @@ function login() {
form.submit();
}
}

View File

@@ -5,6 +5,7 @@ var SEARCH_MAPPING = new Object()
var UNDO = new Object()
var SERVER_CONNECTION = false
var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
// Menü
@@ -51,7 +52,44 @@ function showElement(elmID, type) {
case false: cssClass = "none"; break;
}
document.getElementById(elmID).className = cssClass;
var element = document.getElementById(elmID)
if (element == null) {
return
}
element.className = cssClass;
}
function setConnectionState(state:string, text:string = "") {
var label:string = text
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected"
break
case "busy":
label = "Syncing"
break
case "offline":
label = "Offline"
break
default:
label = "Connecting"
break
}
}
var indicator = document.getElementById("connection-indicator")
if (indicator == null) {
return
}
indicator.className = "status-" + state
indicator.innerText = label
}
function changeButtonAction(element, buttonID, attribute) {
@@ -379,17 +417,19 @@ function searchInMapping() {
function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
var elm = document.getElementById("box-wrapper");
if (elm != null && content != null){
var divs = new Array("myStreamsBox", "clientInfo", "content");
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) {
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
var contentTop = content.getBoundingClientRect().top
var freeSpace = window.innerHeight - contentTop - 26
if (freeSpace < 180) {
freeSpace = 180
}
elm.style.height = window.innerHeight - elementsHeight + "px";
elm.style.height = freeSpace + "px";
}

View File

@@ -37,6 +37,7 @@ class MainMenuItem extends MainMenu {
var item = document.createElement("LI")
item.setAttribute("onclick", "javascript: openThisMenu(this)")
item.setAttribute("id", this.id)
item.setAttribute("data-menu", this.menuKey)
var img = this.createIMG(this.imgSrc)
var value = this.createValue(this.value)
@@ -683,7 +684,7 @@ class ShowContent extends Content {
input.setAttribute("id", "searchMapping")
input.setAttribute("placeholder", "{{.button.search}}")
input.className = "search"
input.setAttribute("onchange", 'javascript: searchInMapping()')
input.setAttribute("oninput", 'javascript: searchInMapping()')
interaction.appendChild(input)
break;
@@ -824,12 +825,157 @@ class ShowContent extends Content {
}
var SHELL_LAYOUT_READY:boolean = false
function setLayoutMenuState(open:boolean) {
if (document.body == null) {
return
}
if (open == true) {
document.body.classList.add("menu-open")
} else {
document.body.classList.remove("menu-open")
}
}
function toggleLayoutMenu() {
if (document.body == null) {
return
}
var isOpen:boolean = document.body.classList.contains("menu-open")
setLayoutMenuState(!isOpen)
}
function closeLayoutMenuIfMobile() {
if (window.innerWidth <= 900) {
setLayoutMenuState(false)
}
}
function setActiveMenu(menuID:string) {
ACTIVE_MENU_ID = menuID.toString()
var menu = document.getElementById("main-menu")
if (menu == null) {
return
}
var items = menu.getElementsByTagName("LI")
for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active")
}
var activeItem = document.getElementById(ACTIVE_MENU_ID)
if (activeItem != null) {
activeItem.classList.add("menu-active")
}
}
function renderStatusCards() {
var wrapper = document.getElementById("status-cards")
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
return
}
var info = SERVER["clientInfo"]
var errors:number = parseInt(info["errors"], 10)
var warnings:number = parseInt(info["warnings"], 10)
var cards:any[] = [
{label: "Streams", value: info["streams"], tone: "ok"},
{label: "EPG Source", value: info["epgSource"], tone: "neutral"},
{label: "XEPG Channels", value: info["xepg"], tone: "ok"},
{label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok"},
{label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok"},
{label: "DVR", value: info["DVR"], tone: "neutral"},
]
wrapper.innerHTML = ""
cards.forEach(card => {
var box = document.createElement("DIV")
box.className = "status-card status-card-" + card.tone
var label = document.createElement("P")
label.className = "status-card-label"
label.innerText = card.label
var value = document.createElement("P")
value.className = "status-card-value"
if (card.value == undefined || card.value == "") {
value.innerText = "-"
} else {
value.innerText = card.value
}
box.appendChild(label)
box.appendChild(value)
wrapper.appendChild(box)
});
}
function initShellLayout() {
if (SHELL_LAYOUT_READY == true) {
return
}
var toggle = document.getElementById("menu-toggle")
if (toggle != null) {
toggle.onclick = function() {
toggleLayoutMenu()
}
}
var overlay = document.getElementById("layout-overlay")
if (overlay != null) {
overlay.onclick = function() {
setLayoutMenuState(false)
}
}
document.addEventListener("keydown", function(event) {
if (event.key == "Escape") {
setLayoutMenuState(false)
showElement("popup", false)
return
}
if (event.key == "/") {
var target = event.target as HTMLElement
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT"
if (onInput == true) {
return
}
var search = document.getElementById("searchMapping")
if (search != null) {
event.preventDefault()
(search as HTMLInputElement).focus()
}
}
});
setConnectionState("idle")
SHELL_LAYOUT_READY = true
}
function PageReady() {
initShellLayout()
var server:Server = new Server("getServerConfig")
server.request(new Object())
window.addEventListener("resize", function(){
if (window.innerWidth > 900) {
setLayoutMenuState(false)
}
calculateWrapperHeight();
}, true);
@@ -853,6 +999,7 @@ function createLayout() {
}
}
renderStatusCards()
if (!document.getElementById("main-menu")) {
return
@@ -889,13 +1036,32 @@ function createLayout() {
}
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID)
}
var content = document.getElementById("content")
var menu = document.getElementById("main-menu")
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
var firstItem = menu.getElementsByTagName("LI")[0]
if (firstItem != undefined) {
firstItem.click()
}
}
}
return
}
function openThisMenu(element) {
var id = element.id
var content:ShowContent = new ShowContent(id)
setActiveMenu(id)
content.show()
closeLayoutMenuIfMobile()
calculateWrapperHeight()
return

View File

@@ -18,6 +18,7 @@ class Server {
if (this.cmd != "updateLog") {
showElement("loading", true)
UNDO = new Object()
setConnectionState("busy")
}
switch(window.location.protocol) {
@@ -36,6 +37,9 @@ class Server {
ws.onopen = function() {
WS_AVAILABLE = true
if (data["cmd"] != "updateLog") {
setConnectionState("busy")
}
console.log("REQUEST (JS):");
console.log(data)
@@ -51,6 +55,7 @@ class Server {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
SERVER_CONNECTION = false
setConnectionState("offline")
if (WS_AVAILABLE == false) {
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
@@ -63,6 +68,9 @@ class Server {
SERVER_CONNECTION = false
showElement("loading", false)
if (data["cmd"] != "updateLog") {
setConnectionState("online")
}
console.log("RESPONSE:");
var response = JSON.parse(e.data);
@@ -74,6 +82,7 @@ class Server {
}
if (response["status"] == false) {
setConnectionState("offline")
alert(response["err"])
@@ -144,4 +153,4 @@ function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
}