Files
xTeVe/ts/base_ts.ts
T
nathan ddc70e170a
continuous-integration/drone/push Build encountered an error
Phase 3: embed web UI with go:embed, pinned TypeScript toolchain, drop i18n, add /healthz
- html/embed.go embeds html/ (pages, css, img, js, video); src/assets.go
  serves it, with os.DirFS("html") under -dev. Static assets get an ETag
  and Cache-Control: no-cache; HTML pages are still templated (only the
  login error message is substituted now).
- Delete the generated src/webUI.go (783 KB base64), src/html-build.go and
  cmd/webui-gen; Dockerfile no longer runs a generator.
- Language layer removed: 254 {{.x}} placeholders inlined as English
  strings in ts/*.ts and the two auth pages; html/lang/en.json, the
  LanguageUI struct and the 'language' setting are gone.
- ts/tsconfig.json + package.json pin typescript 5.9.3; the seven sources
  compile (ES2020, global scripts) into one committed html/js/app.js.
  Ten unreferenced legacy scripts under html/js/ deleted; all pages load
  js/app.js.
- Fix the six type errors that blocked a clean compile, including a real
  bug: a missing semicolon in the search shortcut handler made the code
  call the result of preventDefault(), so the shortcut threw instead of
  focusing the search box.
- /healthz liveness endpoint; Dockerfile healthcheck and README use it.
- Drone: go vet, and a webui-check step that rebuilds the bundle and fails
  if the committed app.js is stale.
- README-DEV.md documents build, UI toolchain, -dev, versioning, CI.
2026-09-26 12:46:45 +10:00

769 lines
17 KiB
TypeScript

var SERVER = new Object()
var BULK_EDIT:Boolean = false
var COLUMN_TO_SORT:number
var SEARCH_MAPPING = new Object()
var UNDO = new Object()
var SERVER_CONNECTION = false
var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
var LAST_BULK_CHECKBOX:HTMLInputElement = null
// Menü
var menuItems = new Array()
menuItems.push(new MainMenuItem("playlist", "Playlist", "m3u.png", "Local or remote playlists"))
//menuItems.push(new MainMenuItem("pmsID", "PMS ID", "number.png", ""))
menuItems.push(new MainMenuItem("filter", "Filter", "filter.png", "Filter playlist"))
menuItems.push(new MainMenuItem("xmltv", "XMLTV", "xmltv.png", "Local or remote XMLTV files"))
menuItems.push(new MainMenuItem("mapping", "Mapping", "mapping.png", "Map playlist channels to EPG channels"))
menuItems.push(new MainMenuItem("users", "Users", "users.png", "User management"))
menuItems.push(new MainMenuItem("settings", "Settings", "settings.png", "Settings"))
menuItems.push(new MainMenuItem("log", "Log", "log.png", "Log"))
menuItems.push(new MainMenuItem("logout", "Logout", "logout.png", "Logout"))
// Kategorien für die Einstellungen
var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("General", "tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("Files", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("Streaming", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("Backup", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("Authentication", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))
function showPopUpElement(elm) {
var allElements = new Array("popup-custom");
for (var i = 0; i < allElements.length; i++) {
showElement(allElements[i], false)
}
showElement(elm, true)
setTimeout(function(){
showElement("popup", true);
}, 10);
return
}
function showElement(elmID, type) {
var cssClass:string
switch(type) {
case true: cssClass = "block"; break;
case false: cssClass = "none"; break;
}
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) {
var value = element.options[element.selectedIndex].value;
document.getElementById(buttonID).setAttribute(attribute, value)
}
function getLocalData(dataType, id):object {
var data = new Object()
switch(dataType) {
case "m3u":
data = SERVER["settings"]["files"][dataType][id]
break
case "hdhr":
data = SERVER["settings"]["files"][dataType][id]
break
case "filter":
case "custom-filter":
case "group-title":
if (id == -1) {
data["active"] = true
data["caseSensitive"] = false
data["description"] = ""
data["exclude"] = ""
data["filter"] = ""
data["include"] = ""
data["name"] = ""
data["type"] = "group-title"
SERVER["settings"]["filter"][id] = data
}
data = SERVER["settings"]["filter"][id]
break
case "xmltv":
data = SERVER["settings"]["files"][dataType][id]
break
case "users":
data = SERVER["users"][id]["data"]
break
case "mapping":
data = SERVER["xepg"]["epgMapping"][id]
break
case "m3uGroups":
data = SERVER["data"]["playlist"]["m3u"]["groups"]
break
}
return data
}
function getObjKeys(obj) {
var keys = new Array();
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
keys.push(i);
}
}
return keys;
}
function getAllSelectedChannels():string[] {
var channels:string[] = new Array()
if (BULK_EDIT == false) {
return channels
}
var trs = document.getElementById("content_table").getElementsByTagName("TR")
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
if ((trs[i].firstChild.firstChild as HTMLInputElement).checked == true) {
channels.push(trs[i].id)
}
}
}
return channels
}
function scheduleChannelRangeSelection(checkbox:HTMLInputElement, event:MouseEvent) {
var shiftPressed = false
if (event != undefined && event.shiftKey == true) {
shiftPressed = true
}
// Run after the native checkbox toggle so we copy the final checked state.
setTimeout(function() {
selectChannelRange(checkbox, shiftPressed)
}, 0)
}
function selectChannelRange(checkbox:HTMLInputElement, shiftPressed:boolean) {
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
return
}
var table = document.getElementById("content_table")
if (table == null) {
return
}
var trs = table.getElementsByTagName("TR")
var visibleCheckboxes:HTMLInputElement[] = new Array()
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
var bulkCheckbox = (trs[i] as HTMLTableRowElement).querySelector("input.bulk") as HTMLInputElement
if (bulkCheckbox != null) {
visibleCheckboxes.push(bulkCheckbox)
}
}
}
var currentIndex = visibleCheckboxes.indexOf(checkbox)
var previousIndex = -1
if (LAST_BULK_CHECKBOX != null) {
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX)
}
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
var start = Math.min(previousIndex, currentIndex)
var end = Math.max(previousIndex, currentIndex)
for (var i = start; i <= end; i++) {
visibleCheckboxes[i].checked = checkbox.checked
}
}
LAST_BULK_CHECKBOX = checkbox
}
function selectAllChannels() {
var bulk:Boolean = false
var trs = document.getElementById("content_table").getElementsByTagName("TR")
if ((trs[0].firstChild.firstChild as HTMLInputElement).checked == true) {
bulk = true
}
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
switch (bulk) {
case true:
(trs[i].firstChild.firstChild as HTMLInputElement).checked = true
break
case false:
(trs[i].firstChild.firstChild as HTMLInputElement).checked = false
break
}
}
}
LAST_BULK_CHECKBOX = null
return
}
function bulkEdit() {
BULK_EDIT = !BULK_EDIT
var className:string
var rows = document.getElementsByClassName("bulk");
switch (BULK_EDIT) {
case true:
className = "bulk showBulk"
break;
case false:
className = "bulk hideBulk"
break;
}
for (var i = 0; i < rows.length; i++) {
rows[i].className = className;
(rows[i] as HTMLInputElement).checked = false
}
LAST_BULK_CHECKBOX = null
return
}
function sortTable(column) {
//console.log(columm);
if (column == COLUMN_TO_SORT) {
return;
}
var table = document.getElementById("content_table");
var tableHead = table.getElementsByTagName("TR")[0];
var tableItems = tableHead.getElementsByTagName("TD");
var sortObj = new Object();
var x, xValue;
var tableHeader
var sortByString = false
if (column > 0 && COLUMN_TO_SORT > 0) {
tableItems[COLUMN_TO_SORT].className = "pointer";
tableItems[column].className = "sortThis";
}
COLUMN_TO_SORT = column;
var rows = (table as HTMLTableElement).rows;
if (rows[1] != undefined) {
tableHeader = rows[0]
x = rows[1].getElementsByTagName("TD")[column];
for (i = 1; i < rows.length; i++) {
x = rows[i].getElementsByTagName("TD")[column];
switch(x.childNodes[0].tagName.toLowerCase()) {
case "input":
xValue = x.getElementsByTagName("INPUT")[0].value.toLowerCase();
break;
case "p":
xValue = x.getElementsByTagName("P")[0].innerText.toLowerCase();
break;
default: console.log(x.childNodes[0].tagName);
}
if (xValue == "" || Number.isNaN(xValue)) {
xValue = i
sortObj[i] = rows[i];
} else {
switch(isNaN(xValue)) {
case false:
xValue = parseFloat(xValue);
sortObj[xValue] = rows[i]
break;
case true:
sortByString = true
sortObj[xValue.toLowerCase() + i] = rows[i]
break;
}
}
}
while (table.firstChild) {
table.removeChild(table.firstChild);
}
var sortValues = getObjKeys(sortObj)
if (sortByString == true) {
sortValues.sort()
console.log(sortValues);
} else {
function sortFloat(a, b) {
return a - b;
}
sortValues.sort(sortFloat);
}
table.appendChild(tableHeader)
for (var i = 0; i < sortValues.length; i++) {
table.appendChild(sortObj[sortValues[i]])
}
}
return
}
function createSearchObj() {
SEARCH_MAPPING = new Object()
var data = SERVER["xepg"]["epgMapping"]
var channels = getObjKeys(data)
var channelKeys:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"]
channels.forEach(id => {
channelKeys.forEach(key => {
if (key == "x-active") {
switch (data[id][key]) {
case true:
SEARCH_MAPPING[id] = "online "
break;
case false:
SEARCH_MAPPING[id] = "offline "
break;
}
} else {
if (key == "x-xmltv-file") {
var xmltvFile = getValueFromProviderFile(data[id][key], "xmltv", "name")
if (xmltvFile != undefined) {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + xmltvFile + " "
}
} else {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
}
}
})
})
return
}
function searchInMapping() {
var searchValue = (document.getElementById("searchMapping") as HTMLInputElement).value;
var trs = document.getElementById("content_table").getElementsByTagName("TR")
for (var i = 1; i < trs.length; ++i) {
var id = trs[i].getAttribute("id")
var element = SEARCH_MAPPING[id]
switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
case true:
document.getElementById(id).style.display = ""
break;
case false:
document.getElementById(id).style.display = "none"
break;
}
}
return
}
function calculateWrapperHeight() {
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 = freeSpace + "px";
}
return
}
function changeChannelNumber(element) {
var dbID = element.parentNode.parentNode.id
var newNumber:number = parseFloat(element.value)
var channelNumbers:number[] = []
var data = SERVER["xepg"]["epgMapping"]
var channels = getObjKeys(data)
if (isNaN(newNumber)) {
alert("Invalid channel number")
return
}
channels.forEach(id => {
var channelNumber = parseFloat(data[id]["x-channelID"])
channelNumbers.push(channelNumber)
})
for (var i = 0; i < channelNumbers.length; i++) {
if (channelNumbers.indexOf(newNumber) == -1) {
break
}
if (Math.floor(newNumber) == newNumber) {
newNumber = newNumber + 1
} else {
newNumber = newNumber + 0.1;
newNumber.toFixed(1)
newNumber = Math.round(newNumber * 10) / 10
}
}
data[dbID]["x-channelID"] = newNumber.toString()
element.value = newNumber
console.log(data[dbID]["x-channelID"])
if (COLUMN_TO_SORT == 1) {
COLUMN_TO_SORT = -1
sortTable(1)
}
return
}
function backup() {
var data = new Object()
console.log("Backup data")
var cmd = "xteveBackup"
console.log("SEND TO SERVER");
console.log(data)
var server:Server = new Server(cmd)
server.request(data)
return
}
function toggleChannelStatus(id:string) {
var element:any
var status:boolean
if(document.getElementById("active")) {
var checkbox = (document.getElementById("active") as HTMLInputElement)
status = (checkbox).checked
}
var ids:string[] = getAllSelectedChannels()
if (ids.length == 0) {
ids.push(id)
}
ids.forEach(id => {
var channel = SERVER["xepg"]["epgMapping"][id]
channel["x-active"] = status
switch (channel["x-active"]) {
case true:
if (channel["x-xmltv-file"] == "-" || channel["x-mapping"] == "-") {
if (BULK_EDIT == false) {
alert(channel["x-name"] + ": Missing XMLTV file / channel")
checkbox.checked = false
}
channel["x-active"] = false
}
break
case false:
// code...
break;
}
if (channel["x-active"] == false) {
document.getElementById(id).className = "notActiveEPG"
} else {
document.getElementById(id).className = "activeEPG"
}
});
}
function restore() {
if (document.getElementById('upload')) {
document.getElementById('upload').remove()
}
var restore = document.createElement("INPUT");
restore.setAttribute("type", "file");
restore.setAttribute("class", "notVisible");
restore.setAttribute("name", "");
restore.id = "upload";
document.body.appendChild(restore);
restore.click();
restore.onchange = function() {
var filename = (restore as HTMLInputElement).files[0].name
var check = confirm("File: " + filename + "\nAll data will be replaced with those from the backup. Should the files be restored?");
if (check == true) {
var reader = new FileReader();
var file = (document.querySelector('input[type=file]') as HTMLInputElement).files[0];
if (file) {
reader.readAsDataURL(file);
reader.onload = function() {
console.log(reader.result);
var data = new Object();
var cmd = "xteveRestore"
data["base64"] = reader.result
var server:Server = new Server(cmd)
server.request(data)
};
} else {
alert("File could not be loaded")
}
restore.remove()
return
}
}
return
}
function uploadLogo() {
if (document.getElementById('upload')) {
document.getElementById('upload').remove()
}
var upload = document.createElement("INPUT");
upload.setAttribute("type", "file");
upload.setAttribute("class", "notVisible");
upload.setAttribute("name", "");
upload.id = "upload";
document.body.appendChild(upload);
upload.click();
upload.onblur = function() {
alert()
}
upload.onchange = function() {
var filename = (upload as HTMLInputElement).files[0].name
var reader = new FileReader();
var file = (document.querySelector('input[type=file]') as HTMLInputElement).files[0];
if (file) {
reader.readAsDataURL(file);
reader.onload = function() {
console.log(reader.result);
var data = new Object();
var cmd = "uploadLogo"
data["base64"] = reader.result
data["filename"] = file.name
var server:Server = new Server(cmd)
server.request(data)
var updateLogo = (document.getElementById('update-icon') as HTMLInputElement)
updateLogo.checked = false
updateLogo.className = "changed"
};
} else {
alert("File could not be loaded")
}
upload.remove()
return
}
}
function checkUndo(key:string) {
switch (key) {
case "epgMapping":
if (UNDO.hasOwnProperty(key)) {
SERVER["xepg"][key] = JSON.parse(JSON.stringify(UNDO[key]))
} else {
UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key]));
}
break;
default:
break;
}
return
}
function sortSelect(elem) {
var tmpAry = [];
var selectedValue = elem[elem.selectedIndex].value;
for (var i=0;i<elem.options.length;i++) tmpAry.push(elem.options[i]);
tmpAry.sort(function(a,b){ return (a.text < b.text)?-1:1; });
while (elem.options.length > 0) elem.options[0] = null;
var newSelectedIndex = 0;
for (var i=0;i<tmpAry.length;i++) {
elem.options[i] = tmpAry[i];
if(elem.options[i].value == selectedValue) newSelectedIndex = i;
}
elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting
return;
}
function updateLog() {
console.log("TOKEN")
var server:Server = new Server("updateLog")
server.request(new Object())
}