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 ## Downloads v2 | 64 Bit only
#### 64 Bit Intel / AMD #### 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> <script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head> </head>
<body onload="javascript: readyForConfiguration(0);"> <body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
<div id="loading" class="block"> <div id="loading" class="block">
<div class="loader"></div> <div class="loader"></div>

View File

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

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; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
-ms-appearance: none; -ms-appearance: none;
font-family: "Arial", sans-serif; box-sizing: border-box;
letter-spacing: 2px; font-family: "Space Grotesk", "Avenir Next", "Trebuchet MS", sans-serif;
letter-spacing: 0.02em;
} }
/*
::-webkit-scrollbar {
display: none;
}
*/
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 11px;
height: 12px; height: 11px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); border-radius: 999px;
border-radius: 5px; background: rgba(7, 17, 29, 0.65);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 5px; border-radius: 999px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0,0.6); border: 2px solid rgba(7, 17, 29, 0.6);
background-color: #444; background: linear-gradient(180deg, #2d597f, #1e4060);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #333; background: linear-gradient(180deg, #3678ac, #20537c);
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background: transparent; background: transparent;
} }
a { html,
color: #00E6FF; 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%);
} }
html, body { body {
color: #fff; line-height: 1.45;
margin: 0px auto; }
height: 100%;
font-size: 14px; a {
color: var(--accent);
}
a:hover {
color: #8de7ff;
}
h1,
h2,
h3,
h4,
h5 {
margin: 0;
font-weight: 600;
letter-spacing: 0.03em;
}
h1 {
font-size: 1.72rem;
} }
h2 { h2 {
font-size: 24px; font-size: 1.35rem;
letter-spacing: 2px;
} }
h3 { h3 {
font-size: 22px; font-size: 1.2rem;
letter-spacing: 1px;
} }
h4 { h4 {
font-size: 20px; font-size: 1.05rem;
letter-spacing: 1px;
line-height: 1.5em;
} }
h5 { h5 {
font-size: 16px; font-size: 0.92rem;
letter-spacing: 1px; margin: 8px 0;
line-height: 1.2em; color: var(--text-muted);
margin: 25px 0px 10px 0px; }
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 { hr {
border: 0; border: 0;
height: 1px; height: 1px;
background: #333; margin: 12px 0;
margin: 10px 0px; background: linear-gradient(90deg, transparent, var(--line), transparent);
}
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;
} }
label { label {
margin-bottom: 20px; margin-bottom: 8px;
display: block; display: block;
} }
li { li {
list-style-type: none; list-style-type: none;
background-color: #111;
padding: 10px 20px;
cursor: pointer; cursor: pointer;
border-left: solid 2px #111; transition: all 0.25s ease;
transition: all 0.3;
} }
li:hover { select,
border-color: #00E6FF input,
textarea,
button {
outline: none;
color: var(--text);
font-size: 13px;
} }
input,
select { 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; cursor: pointer;
width: calc(100% + 2px); border: 1px solid transparent;
border: solid 0px #00E6FF; border-radius: 999px;
border-radius: 0px; color: #03101a;
outline: none; 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; color: #fff;
padding: 9px 10px; background: linear-gradient(135deg, #ff7f7f 0%, #e94343 100%);
display:block; box-shadow: 0 10px 20px rgba(233, 67, 67, 0.35);
background-color: #333;
font-size: 14px;
margin: 5px 0px 5px 0px;
} }
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 { input[type=button].cancel {
color: #ffd8d8;
background-color: transparent; border-color: rgba(255, 111, 111, 0.45);
border-color: red; background: rgba(87, 22, 22, 0.45);
box-shadow: none;
} }
input[type=button].save{ input[type=button].black,
background-color: #111; input[type=submit].black {
float: right; color: #d8ecff;
border-color: var(--line);
background: rgba(14, 32, 50, 0.85);
box-shadow: none;
} }
input[type=button].center {
input[type=button].black, input[type=submit].black{
background-color: #000;
border-color: #000;
}
input[type=button].center{
margin-right: auto; margin-right: auto;
margin-left: 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 { .pointer {
@@ -243,12 +295,11 @@ input[type=button].center{
} }
.pointer:hover { .pointer:hover {
color: #00E6FF; color: var(--accent);
cursor: pointer;
} }
.sortThis { .sortThis {
color: #00E6FF; color: var(--accent);
} }
.w40px { .w40px {
@@ -271,14 +322,9 @@ input[type=button].center{
max-width: 200px; max-width: 200px;
min-width: 100px; min-width: 100px;
width: 200px; width: 200px;
overflow-x: hidden;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} white-space: nowrap;
.w300px {
max-width: 300px;
} }
.w220px { .w220px {
@@ -286,46 +332,18 @@ input[type=button].center{
cursor: alias; cursor: alias;
} }
.w300px {
max-width: 300px;
}
.footer { .footer {
font-size: 10px; font-size: 11px;
} }
.center { .center {
text-align: 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 { .floatRight {
float: right; float: right;
} }
@@ -334,115 +352,311 @@ input[type=button].center{
float: left; 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 { .menu-active {
background-color: #00E6FF; color: #011019;
} }
.menu-notActive { .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 { .menu {
border: solid 1px #00E6FF; border: 1px solid var(--accent);
}
.half {
width: 45%;
}
.screenLogHidden {
transform: translate(0, -110px);
} }
.infoMsg { .infoMsg {
color: #aaa; color: #87b0d1;
} }
.errorMsg { .errorMsg {
color: red; color: var(--error);
} }
.warningMsg { .warningMsg {
color: yellow; color: var(--warn);
} }
.debugMsg { .debugMsg {
color: magenta; color: #d687ff;
} }
.News, .Movie, .Series, .Sports, .Kids { .News,
border-left: solid 2px .Movie,
.Series,
.Sports,
.Kids {
border-left: 3px solid transparent;
padding-left: 8px;
} }
.News { .News {
border-color: tomato border-color: #ff8f6e;
} }
.Movie { .Movie {
border-color: royalblue; border-color: #46a4ff;
} }
.Series { .Series {
border-color: gold; border-color: #ffd267;
} }
.Sports { .Sports {
border-color: yellowgreen; border-color: #5ee495;
} }
.Kids { .Kids {
border-color: mediumpurple; border-color: #f6a8ff;
} }
/* Loading */
#loading { #loading {
left: 0px; position: fixed;
top: 0px; inset: 0;
z-index: 10000; z-index: 2000;
position: absolute; background-color: rgba(5, 12, 20, 0.72);
background-color: rgba(0,0,0, 0.8); backdrop-filter: blur(1.5px);
margin: auto;
width: 100%;
height: 100%;
} }
.loader { .loader {
border: 5px solid transparent; width: 52px;
height: 52px;
border-radius: 50%; border-radius: 50%;
border-top: 5px solid #00E6FF; border: 4px solid transparent;
border-bottom: 5px solid #00E6FF; border-top-color: #58ddff;
width: 50px; border-right-color: #12b9ff;
height: 50px; animation: spin 0.9s linear infinite;
-webkit-animation: spin 1.2s linear infinite;
animation: spin 1.2s linear infinite;
position: fixed; position: fixed;
margin: auto;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
margin: auto;
} }
@-webkit-keyframes spin { #popup {
0% { -webkit-transform: rotate(0deg); } position: fixed;
100% { -webkit-transform: rotate(360deg); } 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 { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } 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> <html>
<head> <head>
<meta charset="utf-8"> <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> <title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css"> <link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css"> <link rel="stylesheet" href="css/base.css" type="text/css">
@@ -17,7 +15,7 @@
</head> </head>
<body onload="javascript: PageReady();"> <body class="app-shell" onload="javascript: PageReady();">
<div id="loading" class="none"> <div id="loading" class="none">
<div class="loader"></div> <div class="loader"></div>
@@ -27,6 +25,7 @@
<div id="popup-custom"></div> <div id="popup-custom"></div>
</div> </div>
<div id="layout-overlay"></div>
<div id="layout"> <div id="layout">
<!-- <!--
@@ -40,13 +39,18 @@
</div> </div>
--> -->
<div id="menu-wrapper" class="layout-left"> <aside id="menu-wrapper" class="layout-left">
<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"></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=""> <table id="clientInfo" class="">
@@ -88,6 +92,8 @@
</table> </table>
<div id="status-cards" class="dashboard-cards"></div>
<div id="myStreamsBox" class="notVisible"> <div id="myStreamsBox" class="notVisible">
<div id="allStreams"> <div id="allStreams">
@@ -99,7 +105,7 @@
<div id="content" class=""></div> <div id="content" class=""></div>
</div> </main>
</div> </div>

View File

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

View File

@@ -5,6 +5,7 @@ var SEARCH_MAPPING = new Object();
var UNDO = new Object(); var UNDO = new Object();
var SERVER_CONNECTION = false; var SERVER_CONNECTION = false;
var WS_AVAILABLE = false; var WS_AVAILABLE = false;
var ACTIVE_MENU_ID = "";
// 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}}"));
@@ -44,7 +45,36 @@ function showElement(elmID, type) {
cssClass = "none"; cssClass = "none";
break; 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) { function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value; var value = element.options[element.selectedIndex].value;
@@ -272,14 +302,15 @@ function searchInMapping() {
return; return;
} }
function calculateWrapperHeight() { function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")) {
var elm = document.getElementById("box-wrapper"); var elm = document.getElementById("box-wrapper");
var divs = new Array("myStreamsBox", "clientInfo", "content"); var content = document.getElementById("content");
var elementsHeight = 0 - elm.offsetHeight; if (elm != null && content != null) {
for (var i = 0; i < divs.length; i++) { var contentTop = content.getBoundingClientRect().top;
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight; var freeSpace = window.innerHeight - contentTop - 26;
if (freeSpace < 180) {
freeSpace = 180;
} }
elm.style.height = window.innerHeight - elementsHeight + "px"; elm.style.height = freeSpace + "px";
} }
return; return;
} }

View File

@@ -43,6 +43,7 @@ var MainMenuItem = /** @class */ (function (_super) {
var item = document.createElement("LI"); var item = document.createElement("LI");
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);
var img = this.createIMG(this.imgSrc); var img = this.createIMG(this.imgSrc);
var value = this.createValue(this.value); var value = this.createValue(this.value);
item.appendChild(img); item.appendChild(img);
@@ -560,7 +561,7 @@ var ShowContent = /** @class */ (function (_super) {
input.setAttribute("id", "searchMapping"); input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "{{.button.search}}"); input.setAttribute("placeholder", "{{.button.search}}");
input.className = "search"; input.className = "search";
input.setAttribute("onchange", 'javascript: searchInMapping()'); input.setAttribute("oninput", 'javascript: searchInMapping()');
interaction.appendChild(input); interaction.appendChild(input);
break; break;
case "settings": case "settings":
@@ -668,10 +669,127 @@ var ShowContent = /** @class */ (function (_super) {
}; };
return ShowContent; return ShowContent;
}(Content)); }(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() { function PageReady() {
initShellLayout();
var server = new Server("getServerConfig"); var server = new Server("getServerConfig");
server.request(new Object()); server.request(new Object());
window.addEventListener("resize", function () { window.addEventListener("resize", function () {
if (window.innerWidth > 900) {
setLayoutMenuState(false);
}
calculateWrapperHeight(); calculateWrapperHeight();
}, true); }, true);
setInterval(function () { setInterval(function () {
@@ -688,6 +806,7 @@ function createLayout() {
document.getElementById(keys[i]).innerHTML = obj[keys[i]]; document.getElementById(keys[i]).innerHTML = obj[keys[i]];
} }
} }
renderStatusCards();
if (!document.getElementById("main-menu")) { if (!document.getElementById("main-menu")) {
return; return;
} }
@@ -713,12 +832,27 @@ function createLayout() {
break; 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; return;
} }
function openThisMenu(element) { function openThisMenu(element) {
var id = element.id; var id = element.id;
var content = new ShowContent(id); var content = new ShowContent(id);
setActiveMenu(id);
content.show(); content.show();
closeLayoutMenuIfMobile();
calculateWrapperHeight(); calculateWrapperHeight();
return; return;
} }

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -5,6 +5,7 @@ var SEARCH_MAPPING = new Object()
var UNDO = new Object() var UNDO = new Object()
var SERVER_CONNECTION = false var SERVER_CONNECTION = false
var WS_AVAILABLE = false var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
// Menü // Menü
@@ -51,7 +52,44 @@ function showElement(elmID, type) {
case false: cssClass = "none"; break; 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) { function changeButtonAction(element, buttonID, attribute) {
@@ -379,17 +417,19 @@ function searchInMapping() {
function calculateWrapperHeight() { function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper"); var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
var divs = new Array("myStreamsBox", "clientInfo", "content"); if (elm != null && content != null){
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) { var contentTop = content.getBoundingClientRect().top
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight; 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") var item = document.createElement("LI")
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)
var img = this.createIMG(this.imgSrc) var img = this.createIMG(this.imgSrc)
var value = this.createValue(this.value) var value = this.createValue(this.value)
@@ -683,7 +684,7 @@ class ShowContent extends Content {
input.setAttribute("id", "searchMapping") input.setAttribute("id", "searchMapping")
input.setAttribute("placeholder", "{{.button.search}}") input.setAttribute("placeholder", "{{.button.search}}")
input.className = "search" input.className = "search"
input.setAttribute("onchange", 'javascript: searchInMapping()') input.setAttribute("oninput", 'javascript: searchInMapping()')
interaction.appendChild(input) interaction.appendChild(input)
break; 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() { function PageReady() {
initShellLayout()
var server:Server = new Server("getServerConfig") var server:Server = new Server("getServerConfig")
server.request(new Object()) server.request(new Object())
window.addEventListener("resize", function(){ window.addEventListener("resize", function(){
if (window.innerWidth > 900) {
setLayoutMenuState(false)
}
calculateWrapperHeight(); calculateWrapperHeight();
}, true); }, true);
@@ -853,6 +999,7 @@ function createLayout() {
} }
} }
renderStatusCards()
if (!document.getElementById("main-menu")) { if (!document.getElementById("main-menu")) {
return 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 return
} }
function openThisMenu(element) { function openThisMenu(element) {
var id = element.id var id = element.id
var content:ShowContent = new ShowContent(id) var content:ShowContent = new ShowContent(id)
setActiveMenu(id)
content.show() content.show()
closeLayoutMenuIfMobile()
calculateWrapperHeight() calculateWrapperHeight()
return return

View File

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