Phase 5b and Phase 6: TypeScript restructure, jsdom tests, regex filters and per-playlist buffer in the UI
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
TypeScript:
- menu_ts.ts split into menu.ts, content.ts, popup.ts and xmltv.ts; the
other files drop their _ts suffix. Bundle order fixed in ts/tsconfig.json.
- All 94 string event handlers (setAttribute("onclick", "javascript: ..."))
replaced with addEventListener closures. changeButtonAction, which
rewrote an onclick attribute from select values, is gone.
- Settings rows are generated from one SETTINGS_FIELDS table instead of
~20 copy-pasted blocks; rendered HTML is byte-identical to before.
saveSettings now serialises password inputs, so a changed Plex token is
actually sent (the server treats the mask as unchanged).
- createLayout rebuilds the menu list only when the set of visible items
changes, so focus in the menu survives a refresh.
- announce()/alertUser(): alerts are mirrored into an aria-live region.
- Filter popup offers a third type, Regular Expression (regex-filter), and
the filter table labels it. Playlist popups get a Buffer select
(default / none / xTeVe / FFmpeg / VLC) saved as the playlist's
"buffer" parameter; the tuner field stays editable when the
playlist's own buffer is active.
Tests: tests/ holds 27 jsdom tests (Node's built-in runner, jsdom pinned)
that load the built bundle with a fixture server payload: menu visibility
rules, mapping table renders names as text, settings panel fields, popup
flows, sorting, bulk select, layout refresh keeps nodes, live region,
regex option, buffer select. npm test runs in the Drone webui-check step.
README-DEV documents the layout and the test workflow.
This commit is contained in:
+44
-7
@@ -60,6 +60,35 @@ function showElement(elmID, type) {
|
||||
element.className = cssClass;
|
||||
}
|
||||
|
||||
// announce writes a message into the ARIA live region so assistive
|
||||
// technology hears it; the region is created when the page has none.
|
||||
function announce(message:string) {
|
||||
if (document.body == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var region = document.getElementById("announcer")
|
||||
if (region == null) {
|
||||
region = document.getElementById("sr-announcer")
|
||||
}
|
||||
if (region == null) {
|
||||
region = document.createElement("DIV")
|
||||
region.id = "announcer"
|
||||
region.className = "sr-only"
|
||||
region.setAttribute("aria-live", "polite")
|
||||
region.setAttribute("aria-atomic", "true")
|
||||
document.body.appendChild(region)
|
||||
}
|
||||
|
||||
region.textContent = message
|
||||
}
|
||||
|
||||
// alertUser shows the browser alert and mirrors it into the live region.
|
||||
function alertUser(message:string) {
|
||||
announce(message)
|
||||
alert(message)
|
||||
}
|
||||
|
||||
function setConnectionState(state:string, text:string = "") {
|
||||
|
||||
var label:string = text
|
||||
@@ -92,9 +121,17 @@ function setConnectionState(state:string, text:string = "") {
|
||||
indicator.innerText = label
|
||||
}
|
||||
|
||||
function changeButtonAction(element, buttonID, attribute) {
|
||||
var value = element.options[element.selectedIndex].value;
|
||||
document.getElementById(buttonID).setAttribute(attribute, value)
|
||||
// markChangedOnChange flags a control as edited on "change" so that
|
||||
// saveSettings / donePopupData pick it up, then runs an optional follow-up.
|
||||
// Callers build many controls through one reused `var`, so the listener must
|
||||
// close over this parameter and not over the caller's variable.
|
||||
function markChangedOnChange(element:HTMLElement, then?:(element:HTMLElement) => void) {
|
||||
element.addEventListener("change", function() {
|
||||
element.className = "changed"
|
||||
if (then != undefined) {
|
||||
then(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getLocalData(dataType, id):object {
|
||||
@@ -501,7 +538,7 @@ function changeChannelNumber(element) {
|
||||
var channels = getObjKeys(data)
|
||||
|
||||
if (isNaN(newNumber)) {
|
||||
alert("Invalid channel number")
|
||||
alertUser("Invalid channel number")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -584,7 +621,7 @@ function toggleChannelStatus(id:string) {
|
||||
if (channel["x-xmltv-file"] == "-" || channel["x-mapping"] == "-") {
|
||||
|
||||
if (BULK_EDIT == false) {
|
||||
alert(channel["x-name"] + ": Missing XMLTV file / channel")
|
||||
alertUser(channel["x-name"] + ": Missing XMLTV file / channel")
|
||||
checkbox.checked = false
|
||||
}
|
||||
|
||||
@@ -649,7 +686,7 @@ function restore() {
|
||||
};
|
||||
|
||||
} else {
|
||||
alert("File could not be loaded")
|
||||
alertUser("File could not be loaded")
|
||||
}
|
||||
|
||||
restore.remove()
|
||||
@@ -707,7 +744,7 @@ function uploadLogo() {
|
||||
};
|
||||
|
||||
} else {
|
||||
alert("File could not be loaded")
|
||||
alertUser("File could not be loaded")
|
||||
}
|
||||
|
||||
upload.remove()
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ function saveWizard() {
|
||||
|
||||
if (value.length == 0) {
|
||||
var msg = name.toUpperCase() + ": " + "Missing input"
|
||||
alert(msg)
|
||||
alertUser(msg)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+764
@@ -0,0 +1,764 @@
|
||||
class Content {
|
||||
|
||||
DocumentID:string = "content"
|
||||
TableID:string = "content_table"
|
||||
DivID:string
|
||||
headerClass:string = "content_table_header"
|
||||
interactionID:string = "content-interaction"
|
||||
|
||||
createHeadline(value):any {
|
||||
var element = document.createElement("H3")
|
||||
element.textContent = value
|
||||
return element
|
||||
}
|
||||
|
||||
createHR():any {
|
||||
var element = document.createElement("HR")
|
||||
return element
|
||||
}
|
||||
|
||||
createInteraction():any {
|
||||
var element = document.createElement("DIV")
|
||||
element.setAttribute("id", this.interactionID)
|
||||
return element
|
||||
}
|
||||
|
||||
createDIV():any {
|
||||
var element = document.createElement("DIV")
|
||||
element.id = this.DivID
|
||||
return element
|
||||
}
|
||||
|
||||
createTABLE():any {
|
||||
var element = document.createElement("TABLE")
|
||||
element.id = this.TableID
|
||||
return element
|
||||
}
|
||||
|
||||
createTableRow():any {
|
||||
var element = document.createElement("TR")
|
||||
element.className = this.headerClass
|
||||
return element
|
||||
}
|
||||
|
||||
createTableContent(menuKey:string):string[] {
|
||||
|
||||
var data = new Object()
|
||||
var rows = new Array()
|
||||
|
||||
switch(menuKey) {
|
||||
case "playlist":
|
||||
var fileTypes = new Array("m3u", "hdhr")
|
||||
|
||||
fileTypes.forEach(fileType => {
|
||||
|
||||
data = SERVER["settings"]["files"][fileType]
|
||||
|
||||
var keys = getObjKeys(data)
|
||||
|
||||
keys.forEach(key => {
|
||||
var tr = document.createElement("TR")
|
||||
tr.id = key
|
||||
|
||||
tr.addEventListener("click", function() { openPopUp(fileType, tr) })
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["name"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
cell.value = data[key]["tuner"]
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["last.update"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["provider.availability"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["type"].toUpperCase();
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["streams"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["group.title"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["tvg.id"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["stream.id"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
rows.push(tr)
|
||||
});
|
||||
|
||||
});
|
||||
break
|
||||
|
||||
case "filter":
|
||||
delete SERVER["settings"]["filter"][-1]
|
||||
data = SERVER["settings"]["filter"]
|
||||
var keys = getObjKeys(data)
|
||||
keys.forEach(key => {
|
||||
var tr = document.createElement("TR")
|
||||
tr.id = key
|
||||
|
||||
tr.addEventListener("click", function() { openPopUp(data[key]["type"], tr) })
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["name"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
switch (data[key]["type"]) {
|
||||
case "custom-filter":
|
||||
cell.value = "Custom"
|
||||
break;
|
||||
|
||||
case "group-title":
|
||||
cell.value = "Group"
|
||||
break;
|
||||
|
||||
case "regex-filter":
|
||||
cell.value = "Regex"
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["filter"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
rows.push(tr)
|
||||
|
||||
});
|
||||
break
|
||||
|
||||
case "xmltv":
|
||||
var fileTypes = new Array("xmltv")
|
||||
|
||||
fileTypes.forEach(fileType => {
|
||||
|
||||
data = SERVER["settings"]["files"][fileType]
|
||||
|
||||
var keys = getObjKeys(data)
|
||||
|
||||
keys.forEach(key => {
|
||||
var tr = document.createElement("TR")
|
||||
|
||||
tr.id = key
|
||||
tr.addEventListener("click", function() { openPopUp(fileType, tr) })
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["name"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["last.update"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["provider.availability"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["xmltv.channels"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["compatibility"]["xmltv.programs"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
rows.push(tr)
|
||||
});
|
||||
|
||||
});
|
||||
break
|
||||
|
||||
case "users":
|
||||
var fileTypes = new Array("users")
|
||||
|
||||
fileTypes.forEach(fileType => {
|
||||
data = SERVER[fileType]
|
||||
|
||||
var keys = getObjKeys(data)
|
||||
|
||||
keys.forEach(key => {
|
||||
var tr = document.createElement("TR")
|
||||
tr.id = key
|
||||
tr.addEventListener("click", function() { openPopUp(fileType, tr) })
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["data"]["username"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = "******"
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (data[key]["data"]["authentication.web"] == true) {
|
||||
cell.value = "✓"
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (data[key]["data"]["authentication.pms"] == true) {
|
||||
cell.value = "✓"
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (data[key]["data"]["authentication.m3u"] == true) {
|
||||
cell.value = "✓"
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (data[key]["data"]["authentication.xml"] == true) {
|
||||
cell.value = "✓"
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (data[key]["data"]["authentication.api"] == true) {
|
||||
cell.value = "✓"
|
||||
} else {
|
||||
cell.value = "-"
|
||||
}
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
rows.push(tr)
|
||||
});
|
||||
|
||||
});
|
||||
break
|
||||
|
||||
case "mapping":
|
||||
BULK_EDIT = false
|
||||
createSearchObj()
|
||||
checkUndo("epgMapping")
|
||||
console.log("MAPPING")
|
||||
data = SERVER["xepg"]["epgMapping"]
|
||||
|
||||
var keys = getObjKeys(data)
|
||||
keys.forEach(key => {
|
||||
var tr = document.createElement("TR")
|
||||
tr.id = key
|
||||
|
||||
|
||||
switch (data[key]["x-active"]) {
|
||||
case true:
|
||||
tr.className = "activeEPG"
|
||||
break;
|
||||
|
||||
case false:
|
||||
tr.className = "notActiveEPG"
|
||||
break;
|
||||
}
|
||||
|
||||
// Bulk
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "BULK"
|
||||
cell.value = false
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// Kanalnummer
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "INPUTCHANNEL"
|
||||
cell.value = data[key]["x-channelID"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// Logo
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "IMG"
|
||||
cell.imageURL = data[key]["tvg-logo"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// Kanalname
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.className = data[key]["x-category"]
|
||||
cell.value = data[key]["x-name"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
|
||||
// Playlist
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
//cell.value = data[key]["_file.m3u.name"]
|
||||
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name")
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
|
||||
// Gruppe (group-title)
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["x-group-title"]
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// XMLTV Datei
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
|
||||
if (data[key]["x-xmltv-file"] != "-") {
|
||||
cell.value = getValueFromProviderFile(data[key]["x-xmltv-file"], "xmltv", "name")
|
||||
} else {
|
||||
cell.value = data[key]["x-xmltv-file"]
|
||||
}
|
||||
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// XMLTV Kanal
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
//var value = str.substring(1, 4);
|
||||
var value = data[key]["x-mapping"]
|
||||
if (value.length > 20) {
|
||||
value = data[key]["x-mapping"].substring(0, 20) + "..."
|
||||
}
|
||||
cell.value = value
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "EDIT"
|
||||
cell.value = "Edit"
|
||||
var editTd = cell.createCell()
|
||||
var editButton = (editTd.firstChild as HTMLInputElement)
|
||||
editButton.addEventListener("click", function() { openPopUp("mapping", editButton) })
|
||||
editButton.setAttribute("id", key)
|
||||
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"])
|
||||
tr.appendChild(editTd)
|
||||
|
||||
rows.push(tr)
|
||||
});
|
||||
|
||||
break
|
||||
|
||||
case "settings":
|
||||
alert()
|
||||
break
|
||||
|
||||
default:
|
||||
console.log("Table content (menuKey):", menuKey);
|
||||
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
return rows
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
class Cell {
|
||||
child:Boolean
|
||||
childType:string
|
||||
value:any
|
||||
className:string
|
||||
tdClassName:string
|
||||
imageURL:string
|
||||
onclick:boolean
|
||||
onclickHandler:() => void
|
||||
|
||||
createCell():any {
|
||||
var td = document.createElement("TD")
|
||||
|
||||
|
||||
if (this.child == true) {
|
||||
var element:any
|
||||
|
||||
switch(this.childType){
|
||||
case "P":
|
||||
element = document.createElement(this.childType);
|
||||
element.textContent = this.value
|
||||
element.className = this.className
|
||||
break
|
||||
|
||||
case "INPUT":
|
||||
element = document.createElement(this.childType);
|
||||
(element as HTMLInputElement).value = this.value;
|
||||
(element as HTMLInputElement).type = "text";
|
||||
break
|
||||
|
||||
case "INPUTCHANNEL":
|
||||
element = document.createElement("INPUT");
|
||||
(element as HTMLInputElement).addEventListener("change", function() { changeChannelNumber(element) });
|
||||
(element as HTMLInputElement).value = this.value;
|
||||
(element as HTMLInputElement).type = "text";
|
||||
break
|
||||
|
||||
case "BULK":
|
||||
element = document.createElement("INPUT");
|
||||
(element as HTMLInputElement).checked = this.value;
|
||||
(element as HTMLInputElement).type = "checkbox";
|
||||
(element as HTMLInputElement).className = "bulk hideBulk";
|
||||
(element as HTMLInputElement).addEventListener("click", function(event) {
|
||||
scheduleChannelRangeSelection((element as HTMLInputElement), (event as MouseEvent))
|
||||
})
|
||||
break
|
||||
|
||||
case "BULK_HEAD":
|
||||
element = document.createElement("INPUT");
|
||||
(element as HTMLInputElement).checked = this.value;
|
||||
(element as HTMLInputElement).type = "checkbox";
|
||||
(element as HTMLInputElement).className = "bulk hideBulk";
|
||||
(element as HTMLInputElement).addEventListener("click", function() { selectAllChannels() })
|
||||
break
|
||||
|
||||
case "IMG":
|
||||
element = document.createElement(this.childType);
|
||||
element.setAttribute("src", this.imageURL)
|
||||
if (this.imageURL != "") {
|
||||
// A broken logo falls back to no image; once, so the empty src cannot loop.
|
||||
element.addEventListener("error", function() { element.setAttribute("src", "") }, {once: true})
|
||||
}
|
||||
break
|
||||
|
||||
case "EDIT":
|
||||
element = document.createElement("INPUT");
|
||||
(element as HTMLInputElement).type = "button";
|
||||
(element as HTMLInputElement).value = this.value;
|
||||
(element as HTMLInputElement).className = "mapping-edit-button";
|
||||
break
|
||||
}
|
||||
|
||||
td.appendChild(element)
|
||||
|
||||
} else {
|
||||
td.textContent = this.value
|
||||
}
|
||||
|
||||
if (this.onclick == true) {
|
||||
td.addEventListener("click", this.onclickHandler)
|
||||
td.className = "pointer"
|
||||
}
|
||||
|
||||
if (this.tdClassName != undefined) {
|
||||
td.className = this.tdClassName
|
||||
}
|
||||
|
||||
return td
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
class ShowContent extends Content {
|
||||
menuID:number
|
||||
|
||||
constructor(menuID:number) {
|
||||
super()
|
||||
this.menuID = menuID
|
||||
}
|
||||
|
||||
createInput(type:string, name:string, value:string,):any {
|
||||
|
||||
var input = document.createElement("INPUT")
|
||||
input.setAttribute("type", type)
|
||||
input.setAttribute("name", name)
|
||||
input.setAttribute("value", value)
|
||||
return input
|
||||
}
|
||||
|
||||
show():void {
|
||||
COLUMN_TO_SORT = -1
|
||||
// Alten Inhalt löschen
|
||||
var doc = document.getElementById(this.DocumentID)
|
||||
doc.innerHTML = ""
|
||||
showPreview(false)
|
||||
|
||||
// Überschrift
|
||||
var headline:string[] = menuItems[this.menuID].headline
|
||||
|
||||
var menuKey = menuItems[this.menuID].menuKey
|
||||
var h = this.createHeadline(headline)
|
||||
doc.appendChild(h)
|
||||
|
||||
var hr = this.createHR()
|
||||
doc.appendChild(hr)
|
||||
|
||||
// Interaktion
|
||||
var div =this.createInteraction()
|
||||
doc.appendChild(div)
|
||||
var interaction = document.getElementById(this.interactionID)
|
||||
switch (menuKey) {
|
||||
case "playlist":
|
||||
var input = this.createInput("button", menuKey, "New")
|
||||
input.setAttribute("id", "-")
|
||||
input.addEventListener("click", function() { openPopUp("playlist") })
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
case "filter":
|
||||
var input = this.createInput("button", menuKey, "New")
|
||||
input.setAttribute("id", -1)
|
||||
input.addEventListener("click", function(event) { openPopUp("filter", event.currentTarget) })
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
|
||||
case "xmltv":
|
||||
var input = this.createInput("button", menuKey, "New")
|
||||
input.setAttribute("id", "xmltv")
|
||||
input.addEventListener("click", function() { openPopUp("xmltv") })
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
case "users":
|
||||
var input = this.createInput("button", menuKey, "New")
|
||||
input.setAttribute("id", "users")
|
||||
input.addEventListener("click", function() { openPopUp("users") })
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
case "mapping":
|
||||
showElement("loading", true)
|
||||
var input = this.createInput("button", menuKey, "Save")
|
||||
input.addEventListener("click", function() { savePopupData("mapping", "", false, 0) })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var input = this.createInput("button", menuKey, "Bulk Edit")
|
||||
input.addEventListener("click", function() { bulkEdit() })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var input = this.createInput("search", "search", "")
|
||||
input.setAttribute("id", "searchMapping")
|
||||
input.setAttribute("placeholder", "Search")
|
||||
input.className = "search"
|
||||
input.addEventListener("input", function() { searchInMapping() })
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
case "settings":
|
||||
var input = this.createInput("button", menuKey, "Save")
|
||||
input.addEventListener("click", function() { saveSettings() })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var input = this.createInput("button", menuKey, "Backup")
|
||||
input.addEventListener("click", function() { backup() })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var input = this.createInput("button", menuKey, "Restore")
|
||||
input.addEventListener("click", function() { restore() })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var wrapper = document.createElement("DIV")
|
||||
wrapper.setAttribute("id", "box-wrapper")
|
||||
doc.appendChild(wrapper)
|
||||
|
||||
this.DivID = "content_settings"
|
||||
var settings = this.createDIV()
|
||||
wrapper.appendChild(settings)
|
||||
|
||||
showSettings()
|
||||
|
||||
return
|
||||
break
|
||||
|
||||
case "log":
|
||||
var input = this.createInput("button", menuKey, "Reset Logs")
|
||||
input.addEventListener("click", function() { resetLogs() })
|
||||
interaction.appendChild(input)
|
||||
|
||||
var wrapper = document.createElement("DIV")
|
||||
wrapper.setAttribute("id", "box-wrapper")
|
||||
doc.appendChild(wrapper)
|
||||
|
||||
this.DivID = "content_log"
|
||||
var logs = this.createDIV()
|
||||
wrapper.appendChild(logs)
|
||||
|
||||
showLogs(true)
|
||||
|
||||
return
|
||||
break
|
||||
|
||||
case "logout":
|
||||
location.reload()
|
||||
document.cookie = "Token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
break
|
||||
|
||||
default:
|
||||
console.log("Show content (menuKey):", menuKey);
|
||||
break;
|
||||
}
|
||||
|
||||
// Tabelle erstellen (falls benötigt)
|
||||
var tableHeader:string[] = menuItems[this.menuID].tableHeader
|
||||
if (tableHeader.length > 0) {
|
||||
var wrapper = document.createElement("DIV")
|
||||
doc.appendChild(wrapper)
|
||||
wrapper.setAttribute("id", "box-wrapper")
|
||||
|
||||
var table = this.createTABLE()
|
||||
wrapper.appendChild(table)
|
||||
|
||||
var header = this.createTableRow()
|
||||
table.appendChild(header)
|
||||
|
||||
// Kopfzeile der Tablle
|
||||
tableHeader.forEach(element => {
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = element
|
||||
if (element == "BULK") {
|
||||
cell.childType = "BULK_HEAD";
|
||||
cell.value = false
|
||||
}
|
||||
|
||||
if (menuKey == "mapping") {
|
||||
|
||||
if (element == "Ch. No.") {
|
||||
cell.onclick = true
|
||||
cell.onclickHandler = function() { sortTable(1) }
|
||||
cell.tdClassName = "sortThis"
|
||||
}
|
||||
|
||||
if (element == "Channel Name") {
|
||||
cell.onclick = true
|
||||
cell.onclickHandler = function() { sortTable(3) }
|
||||
}
|
||||
|
||||
if (element == "Playlist") {
|
||||
cell.onclick = true
|
||||
cell.onclickHandler = function() { sortTable(4) }
|
||||
}
|
||||
|
||||
if (element == "Group Title") {
|
||||
cell.onclick = true
|
||||
cell.onclickHandler = function() { sortTable(5) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
header.appendChild(cell.createCell())
|
||||
});
|
||||
|
||||
table.appendChild(header)
|
||||
|
||||
// Inhalt der Tabelle
|
||||
var rows:any = this.createTableContent(menuKey)
|
||||
rows.forEach(tr => {
|
||||
table.appendChild(tr)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
switch (menuKey) {
|
||||
case "mapping":
|
||||
sortTable(1)
|
||||
break;
|
||||
|
||||
case "filter":
|
||||
showPreview(true)
|
||||
sortTable(0)
|
||||
break
|
||||
|
||||
default:
|
||||
COLUMN_TO_SORT = -1
|
||||
sortTable(0)
|
||||
break;
|
||||
}
|
||||
|
||||
showElement("loading", false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function openThisMenu(element) {
|
||||
var id = element.id
|
||||
var content:ShowContent = new ShowContent(id)
|
||||
setActiveMenu(id)
|
||||
content.show()
|
||||
closeLayoutMenuIfMobile()
|
||||
calculateWrapperHeight()
|
||||
|
||||
return
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
class MainMenu {
|
||||
DocumentID:string = "main-menu"
|
||||
HTMLTag:string = "LI"
|
||||
ImagePath:string = "img/"
|
||||
|
||||
createIMG(src):any {
|
||||
var element = document.createElement("IMG")
|
||||
element.setAttribute("src", this.ImagePath + src)
|
||||
return element
|
||||
}
|
||||
|
||||
createValue(value):any {
|
||||
var element = document.createElement("P")
|
||||
element.textContent = value
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
class MainMenuItem extends MainMenu {
|
||||
menuKey:string
|
||||
value:string
|
||||
imgSrc:string
|
||||
headline:string
|
||||
id:string
|
||||
tableHeader:string[]
|
||||
|
||||
constructor(menuKey:string, value:string, image:string, headline:string) {
|
||||
super()
|
||||
this.menuKey = menuKey
|
||||
this.value = value
|
||||
this.imgSrc = image
|
||||
this.headline = headline
|
||||
}
|
||||
|
||||
createItem():void {
|
||||
var item = document.createElement("LI")
|
||||
item.addEventListener("click", function() { openThisMenu(item) })
|
||||
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)
|
||||
item.appendChild(value)
|
||||
|
||||
var doc = document.getElementById(this.DocumentID)
|
||||
doc.appendChild(item)
|
||||
|
||||
switch(this.menuKey) {
|
||||
case "playlist":
|
||||
this.tableHeader = ["Playlist", "Tuner", "Last Update", "Availability %", "Type", "Streams", "group-title %", "tvg-id %", "Unique ID %"]
|
||||
break
|
||||
|
||||
case "xmltv":
|
||||
this.tableHeader = ["Guide", "Last Update", "Availability %", "Channels", "Programs"]
|
||||
break
|
||||
|
||||
case "filter":
|
||||
this.tableHeader = ["Filter Name", "Filter Type", "Filter"]
|
||||
break
|
||||
|
||||
case "users":
|
||||
this.tableHeader = ["Username", "Password", "WEB", "PMS", "M3U", "XML", "API"]
|
||||
break
|
||||
|
||||
case "mapping":
|
||||
this.tableHeader = ["BULK", "Ch. No.", "Logo", "Channel Name", "Playlist", "Group Title", "XMLTV File", "XMLTV ID", "Edit"]
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
//console.log(this.menuKey, this.tableHeader);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
var activeMenuKey:string = ""
|
||||
if (activeItem != null) {
|
||||
activeItem.classList.add("menu-active")
|
||||
var menuKeyValue = activeItem.getAttribute("data-menu")
|
||||
if (menuKeyValue != null) {
|
||||
activeMenuKey = menuKeyValue
|
||||
}
|
||||
}
|
||||
|
||||
if (document.body != null) {
|
||||
if (activeMenuKey.length > 0) {
|
||||
document.body.setAttribute("data-active-menu", activeMenuKey)
|
||||
} else {
|
||||
document.body.removeAttribute("data-active-menu")
|
||||
}
|
||||
|
||||
if (activeMenuKey == "log") {
|
||||
document.body.classList.add("menu-log-focus")
|
||||
} else {
|
||||
document.body.classList.remove("menu-log-focus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 shouldPollLogs():boolean {
|
||||
if (document.hidden == true) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById("content_log") == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU_ID.length < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
var activeItem = document.getElementById(ACTIVE_MENU_ID)
|
||||
if (activeItem == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return activeItem.getAttribute("data-menu") == "log"
|
||||
}
|
||||
|
||||
function PageReady() {
|
||||
|
||||
initShellLayout()
|
||||
|
||||
var server:Server = new Server("getServerConfig")
|
||||
server.request(new Object())
|
||||
|
||||
var bootstrapAttempts:number = 0
|
||||
var maxBootstrapAttempts:number = 5
|
||||
var bootstrapTimer:number = window.setInterval(function() {
|
||||
|
||||
if (SERVER.hasOwnProperty("clientInfo") == true) {
|
||||
window.clearInterval(bootstrapTimer)
|
||||
return
|
||||
}
|
||||
|
||||
if (WS.hasQueued("getServerConfig")) {
|
||||
return
|
||||
}
|
||||
|
||||
bootstrapAttempts++
|
||||
var retryServer:Server = new Server("getServerConfig")
|
||||
retryServer.request(new Object())
|
||||
|
||||
if (bootstrapAttempts >= maxBootstrapAttempts) {
|
||||
window.clearInterval(bootstrapTimer)
|
||||
}
|
||||
|
||||
}, 3000)
|
||||
|
||||
window.addEventListener("resize", function(){
|
||||
if (window.innerWidth > 900) {
|
||||
setLayoutMenuState(false)
|
||||
}
|
||||
calculateWrapperHeight();
|
||||
}, true);
|
||||
|
||||
setInterval(function(){
|
||||
if (shouldPollLogs() == false) {
|
||||
return
|
||||
}
|
||||
updateLog()
|
||||
}, 10000);
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function isClientInfoHttpURL(value:string):boolean {
|
||||
return /^https?:\/\//i.test(value)
|
||||
}
|
||||
|
||||
function setClientInfoValue(key:string, value:any) {
|
||||
var element = document.getElementById(key)
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var textValue = ""
|
||||
if (value != undefined && value != null) {
|
||||
textValue = String(value)
|
||||
}
|
||||
|
||||
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
|
||||
element.innerHTML = ""
|
||||
var anchor = document.createElement("a")
|
||||
anchor.href = textValue
|
||||
anchor.target = "_blank"
|
||||
anchor.rel = "noopener noreferrer"
|
||||
anchor.textContent = textValue
|
||||
element.appendChild(anchor)
|
||||
return
|
||||
}
|
||||
|
||||
element.textContent = textValue
|
||||
}
|
||||
|
||||
// menuItemVisible reports whether a menu entry applies to the current server state.
|
||||
function menuItemVisible(item:MainMenuItem):boolean {
|
||||
switch (item.menuKey) {
|
||||
|
||||
case "users":
|
||||
case "logout":
|
||||
return SERVER["settings"]["authentication.web"] == true
|
||||
|
||||
case "mapping":
|
||||
case "xmltv":
|
||||
return SERVER["clientInfo"]["epgSource"] == "XEPG"
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// renderedMenuKeys lists the menu entries currently in the document, in order.
|
||||
function renderedMenuKeys(menu:HTMLElement):string[] {
|
||||
var keys:string[] = []
|
||||
var items = menu.getElementsByTagName("LI")
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
keys.push(items[i].getAttribute("data-menu"))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function createLayout() {
|
||||
|
||||
// Client Info
|
||||
var obj = SERVER["clientInfo"]
|
||||
var keys = getObjKeys(obj);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
|
||||
setClientInfoValue(keys[i], obj[keys[i]])
|
||||
|
||||
}
|
||||
renderStatusCards()
|
||||
|
||||
var menu = document.getElementById("main-menu")
|
||||
if (menu == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Menu: rebuilt only when the set of visible entries changes, so that
|
||||
// keyboard focus inside the list survives routine server responses.
|
||||
var visibleKeys:string[] = []
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
menuItems[i].id = i
|
||||
if (menuItemVisible(menuItems[i])) {
|
||||
visibleKeys.push(menuItems[i].menuKey)
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleKeys.join(",") != renderedMenuKeys(menu).join(",")) {
|
||||
menu.innerHTML = ""
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItemVisible(menuItems[i])) {
|
||||
menuItems[i].createItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
|
||||
setActiveMenu(ACTIVE_MENU_ID)
|
||||
}
|
||||
|
||||
var content = document.getElementById("content")
|
||||
if (ACTIVE_MENU_ID.length == 0 && content != null) {
|
||||
|
||||
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
|
||||
var firstItem = menu.getElementsByTagName("LI")[0] as HTMLElement
|
||||
if (firstItem != undefined) {
|
||||
firstItem.click()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
-2477
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -228,7 +228,7 @@ class Server {
|
||||
|
||||
if (response["status"] == false) {
|
||||
setConnectionState("offline")
|
||||
alert(response["err"])
|
||||
alertUser(response["err"])
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload()
|
||||
}
|
||||
@@ -266,7 +266,7 @@ class Server {
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("alert")) {
|
||||
alert(response["alert"])
|
||||
alertUser(response["alert"])
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
@@ -295,7 +295,7 @@ class Server {
|
||||
setConnectionState(state)
|
||||
|
||||
if (WS_AVAILABLE == false && !isLogUpdate && requestCmd != "getServerConfig") {
|
||||
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
|
||||
alertUser("No websocket connection to xTeVe could be established. Check your network configuration.")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
+1171
File diff suppressed because it is too large
Load Diff
+171
-543
@@ -1,3 +1,119 @@
|
||||
// One entry per settings key. The categories in base.ts decide which keys
|
||||
// are shown and in which order; this table decides how each one renders.
|
||||
interface SettingsFieldOption {
|
||||
text:string
|
||||
value:string
|
||||
}
|
||||
|
||||
interface SettingsField {
|
||||
key:string
|
||||
title:string
|
||||
type:"text" | "checkbox" | "number" | "select" | "password"
|
||||
placeholder?:string
|
||||
autocomplete?:string
|
||||
options?:SettingsFieldOption[]
|
||||
// Static text written in the source; may contain HTML.
|
||||
description:string
|
||||
}
|
||||
|
||||
function settingsOptions(text:string[], values:string[]):SettingsFieldOption[] {
|
||||
var options:SettingsFieldOption[] = []
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
options.push({text: text[i], value: values[i]})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function settingsNumberOptions(from:number, to:number):SettingsFieldOption[] {
|
||||
var options:SettingsFieldOption[] = []
|
||||
for (var i = from; i <= to; i++) {
|
||||
options.push({text: i.toString(), value: i.toString()})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const SETTINGS_FIELDS:SettingsField[] = [
|
||||
|
||||
// Text inputs
|
||||
{key: "update", title: "Schedule for updating (Playlist, XMLTV, Backup)", type: "text", placeholder: "0000,1000,2000",
|
||||
description: "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated. Leave this field empty if no updates are to be carried out."},
|
||||
{key: "backup.path", title: "Location for automatic backups", type: "text", placeholder: "/mnt/data/backup/xteve/",
|
||||
description: "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder."},
|
||||
{key: "temp.path", title: "Location for the temporary files", type: "text", placeholder: "/tmp/xteve/",
|
||||
description: "Location for the buffer files."},
|
||||
{key: "user.agent", title: "User Agent", type: "text", placeholder: "xTeVe",
|
||||
description: "User Agent for HTTP requests. For every HTTP connection, this value is used for the user agent. Should only be changed if xTeVe is blocked."},
|
||||
{key: "plex.url", title: "Plex Server URL", type: "text", placeholder: "http://plex-host:32400",
|
||||
description: "Base URL of your Plex server. Example: http://192.168.1.10:32400"},
|
||||
// The server sends the mask "********" and treats it coming back as "unchanged".
|
||||
{key: "plex.token", title: "Plex API Token", type: "password", placeholder: "Plex X-Plex-Token", autocomplete: "off",
|
||||
description: "Plex token used for authenticated API calls to refresh your DVR guide."},
|
||||
{key: "buffer.timeout", title: "Timeout for new client connections", type: "text", placeholder: "100",
|
||||
description: "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds."},
|
||||
{key: "ffmpeg.path", title: "FFmpeg Binary Path", type: "text", placeholder: "/path/to/ffmpeg",
|
||||
description: "Path to FFmpeg binary."},
|
||||
{key: "ffmpeg.options", title: "FFmpeg Options", type: "text", placeholder: "Leave blank to set default settings",
|
||||
description: "FFmpeg options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."},
|
||||
{key: "vlc.path", title: "VLC / CVLC Binary Path", type: "text", placeholder: "/path/to/cvlc",
|
||||
description: "Path to VLC / CVLC binary."},
|
||||
{key: "vlc.options", title: "VLC / CVLC Options", type: "text", placeholder: "Leave blank to set default settings",
|
||||
description: "VLC / CVLC options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."},
|
||||
{key: "udpxy", title: "UDPxy address", type: "text", placeholder: "host:port",
|
||||
description: "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service."},
|
||||
|
||||
// Checkboxes
|
||||
{key: "authentication.web", title: "WEB Authentication", type: "checkbox",
|
||||
description: "Access to the web interface only possible with credentials."},
|
||||
{key: "authentication.pms", title: "PMS Authentication", type: "checkbox",
|
||||
description: "Plex requests are only possible with authentication. <br><b>Warning!!!</b> After activating this function xTeVe must be delete in the PMS DVR settings and set up again."},
|
||||
{key: "authentication.m3u", title: "M3U Authentication", type: "checkbox",
|
||||
description: "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."},
|
||||
{key: "authentication.xml", title: "XML Authentication", type: "checkbox",
|
||||
description: "Downloading the xteve.xml file via an HTTP request is only possible with authentication"},
|
||||
// Only rendered while web authentication is on (see SettingsCategoryItem.createCategory).
|
||||
{key: "authentication.api", title: "API Authentication", type: "checkbox",
|
||||
description: "Access to the API interface is only possible with authentication."},
|
||||
{key: "files.update", title: "Updates all files at startup", type: "checkbox",
|
||||
description: "Updates all playlists, tuner and XMLTV files at startup."},
|
||||
{key: "cache.images", title: "Image Caching", type: "checkbox",
|
||||
description: "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.<br>Downloading the images may take a while and will be done in the background."},
|
||||
{key: "xepg.replace.missing.images", title: "Replace missing program images", type: "checkbox",
|
||||
description: "If the poster in the XMLTV program is missing, the channel logo will be used."},
|
||||
{key: "api", title: "API Interface", type: "checkbox",
|
||||
description: "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"},
|
||||
{key: "use_plexAPI", title: "Use Plex API Refresh", type: "checkbox",
|
||||
description: "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."},
|
||||
|
||||
// Selects
|
||||
{key: "xepg.missing.epg.mode", title: "Missing EPG Handling", type: "select",
|
||||
options: settingsOptions(["Strict (deactivate channels)", "Relaxed (keep active / dummy guide)"], ["strict", "relaxed"]),
|
||||
description: "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved."},
|
||||
{key: "tuner", title: "Number of Tuners", type: "select",
|
||||
options: settingsNumberOptions(1, 100),
|
||||
description: "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."},
|
||||
{key: "epgSource", title: "EPG Source", type: "select",
|
||||
options: settingsOptions(["PMS", "XEPG"], ["PMS", "XEPG"]),
|
||||
description: "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"},
|
||||
{key: "backup.keep", title: "Number of backups to keep", type: "select",
|
||||
options: settingsOptions(["5", "10", "20", "30", "40", "50"], ["5", "10", "20", "30", "40", "50"]),
|
||||
description: "Number of backups to keep. Older backups are automatically deleted."},
|
||||
{key: "buffer.size.kb", title: "Buffer Size", type: "select",
|
||||
options: settingsOptions(["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"], ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"]),
|
||||
description: "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."},
|
||||
{key: "buffer", title: "Stream Buffer", type: "select",
|
||||
options: settingsOptions(["No Buffer (Client connects to the streaming server)", "xTeVe: (xTeVe connects to the streaming server)", "FFmpeg: (FFmpeg connects to the streaming server)", "VLC: (VLC connects to the streaming server)"], ["-", "xteve", "ffmpeg", "vlc"]),
|
||||
description: "Functions of the buffer:<br>- The stream is passed from xTeVe, FFmpeg or VLC to Plex, Emby or M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- RTP / RTPS support (only FFmpeg or VLC)<br>- Re-streaming<br>- Separate tuner limit for each playlist"},
|
||||
]
|
||||
|
||||
function getSettingsField(settingsKey:string):SettingsField {
|
||||
for (var i = 0; i < SETTINGS_FIELDS.length; i++) {
|
||||
if (SETTINGS_FIELDS[i].key == settingsKey) {
|
||||
return SETTINGS_FIELDS[i]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
class SettingsCategory {
|
||||
DocumentID:string = "content_settings"
|
||||
createCategoryHeadline(value:string):any {
|
||||
@@ -11,558 +127,70 @@ class SettingsCategory {
|
||||
return element
|
||||
}
|
||||
|
||||
createSettings(settingsKey:string):any {
|
||||
var setting = document.createElement("TR")
|
||||
// createControl builds the input for one field, filled with the current value.
|
||||
createControl(field:SettingsField, data:any):HTMLElement {
|
||||
var content:PopupContent = new PopupContent()
|
||||
var data = SERVER["settings"][settingsKey]
|
||||
var control:HTMLElement
|
||||
|
||||
switch (settingsKey) {
|
||||
switch (field.type) {
|
||||
|
||||
// Texteingaben
|
||||
case "update":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Schedule for updating (Playlist, XMLTV, Backup)" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "update", data.toString())
|
||||
input.setAttribute("placeholder", "0000,1000,2000")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
case "checkbox":
|
||||
control = content.createCheckbox(field.key);
|
||||
(control as HTMLInputElement).checked = data
|
||||
break
|
||||
|
||||
case "backup.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Location for automatic backups" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "backup.path", data)
|
||||
input.setAttribute("placeholder", "/mnt/data/backup/xteve/")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "temp.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Location for the temporary files" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "temp.path", data)
|
||||
input.setAttribute("placeholder", "/tmp/xteve/")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "user.agent":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "User Agent" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "user.agent", data)
|
||||
input.setAttribute("placeholder", "xTeVe")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "plex.url":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Plex Server URL" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "plex.url", data)
|
||||
input.setAttribute("placeholder", "http://plex-host:32400")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "plex.token":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Plex API Token" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("password", "plex.token", data)
|
||||
input.setAttribute("placeholder", "Plex X-Plex-Token")
|
||||
input.setAttribute("autocomplete", "off")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer.timeout":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Timeout for new client connections" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "buffer.timeout", data)
|
||||
input.setAttribute("placeholder", "100")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "ffmpeg.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "FFmpeg Binary Path" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "ffmpeg.path", data)
|
||||
input.setAttribute("placeholder", "/path/to/ffmpeg")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "ffmpeg.options":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "FFmpeg Options" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "ffmpeg.options", data)
|
||||
input.setAttribute("placeholder", "Leave blank to set default settings")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "vlc.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "VLC / CVLC Binary Path" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "vlc.path", data)
|
||||
input.setAttribute("placeholder", "/path/to/cvlc")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "vlc.options":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "VLC / CVLC Options" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "vlc.options", data)
|
||||
input.setAttribute("placeholder", "Leave blank to set default settings")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
// Checkboxen
|
||||
case "authentication.web":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "WEB Authentication" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "authentication.pms":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "PMS Authentication" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "authentication.m3u":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "M3U Authentication" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "authentication.xml":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "XML Authentication" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "authentication.api":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "API Authentication" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "files.update":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Updates all files at startup" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "cache.images":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Image Caching" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "xepg.replace.missing.images":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Replace missing program images" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "api":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "API Interface" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "use_plexAPI":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Use Plex API Refresh" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
// Select
|
||||
case "xepg.missing.epg.mode":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Missing EPG Handling" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["Strict (deactivate channels)", "Relaxed (keep active / dummy guide)"]
|
||||
var values:any[] = ["strict", "relaxed"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "tuner":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Number of Tuners" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text = new Array()
|
||||
var values = new Array()
|
||||
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
text.push(i)
|
||||
values.push(i)
|
||||
}
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "epgSource":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "EPG Source" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["PMS", "XEPG"]
|
||||
var values:any[] = ["PMS", "XEPG"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "backup.keep":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Number of backups to keep" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["5", "10", "20", "30", "40", "50"]
|
||||
var values:any[] = ["5", "10", "20", "30", "40", "50"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer.size.kb":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Buffer Size" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"]
|
||||
var values:any[] = ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "Stream Buffer" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["No Buffer (Client connects to the streaming server)", "xTeVe: (xTeVe connects to the streaming server)", "FFmpeg: (FFmpeg connects to the streaming server)", "VLC: (VLC connects to the streaming server)"]
|
||||
var values:any[] = ["-", "xteve", "ffmpeg", "vlc"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "udpxy":
|
||||
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "UDPxy address" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "udpxy", data)
|
||||
input.setAttribute("placeholder", "host:port")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
return setting
|
||||
|
||||
}
|
||||
|
||||
|
||||
createDescription(settingsKey:string):any {
|
||||
|
||||
var description = document.createElement("TR")
|
||||
var text:string
|
||||
switch (settingsKey) {
|
||||
|
||||
case "authentication.web":
|
||||
text = "Access to the web interface only possible with credentials."
|
||||
break
|
||||
|
||||
case "authentication.m3u":
|
||||
text = "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."
|
||||
break
|
||||
|
||||
case "authentication.pms":
|
||||
text = "Plex requests are only possible with authentication. <br><b>Warning!!!</b> After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
|
||||
break
|
||||
|
||||
case "authentication.xml":
|
||||
text = "Downloading the xteve.xml file via an HTTP request is only possible with authentication"
|
||||
break
|
||||
|
||||
case "authentication.api":
|
||||
if (SERVER["settings"]["authentication.web"] == true) {
|
||||
text = "Access to the API interface is only possible with authentication."
|
||||
}
|
||||
break
|
||||
|
||||
case "backup.keep":
|
||||
text = "Number of backups to keep. Older backups are automatically deleted."
|
||||
break
|
||||
|
||||
case "backup.path":
|
||||
text = "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder."
|
||||
break
|
||||
|
||||
case "temp.path":
|
||||
text = "Location for the buffer files."
|
||||
break
|
||||
|
||||
case "buffer":
|
||||
text = "Functions of the buffer:<br>- The stream is passed from xTeVe, FFmpeg or VLC to Plex, Emby or M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- RTP / RTPS support (only FFmpeg or VLC)<br>- Re-streaming<br>- Separate tuner limit for each playlist"
|
||||
break
|
||||
|
||||
case "buffer.size.kb":
|
||||
text = "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."
|
||||
break
|
||||
|
||||
case "buffer.timeout":
|
||||
text = "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds."
|
||||
break
|
||||
|
||||
case "user.agent":
|
||||
text = "User Agent for HTTP requests. For every HTTP connection, this value is used for the user agent. Should only be changed if xTeVe is blocked."
|
||||
break
|
||||
|
||||
case "plex.url":
|
||||
text = "Base URL of your Plex server. Example: http://192.168.1.10:32400"
|
||||
break
|
||||
|
||||
case "plex.token":
|
||||
text = "Plex token used for authenticated API calls to refresh your DVR guide."
|
||||
break
|
||||
|
||||
case "ffmpeg.path":
|
||||
text = "Path to FFmpeg binary."
|
||||
break
|
||||
|
||||
case "ffmpeg.options":
|
||||
text = "FFmpeg options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."
|
||||
break
|
||||
|
||||
case "vlc.path":
|
||||
text = "Path to VLC / CVLC binary."
|
||||
break
|
||||
|
||||
case "vlc.options":
|
||||
text = "VLC / CVLC options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings."
|
||||
break
|
||||
|
||||
case "epgSource":
|
||||
text = "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
break
|
||||
|
||||
case "tuner":
|
||||
text = "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
|
||||
break
|
||||
|
||||
case "update":
|
||||
text = "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated. Leave this field empty if no updates are to be carried out."
|
||||
break
|
||||
|
||||
case "api":
|
||||
text = "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
|
||||
break
|
||||
|
||||
case "use_plexAPI":
|
||||
text = "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."
|
||||
break
|
||||
|
||||
case "files.update":
|
||||
text = "Updates all playlists, tuner and XMLTV files at startup."
|
||||
break
|
||||
|
||||
case "cache.images":
|
||||
text = "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.<br>Downloading the images may take a while and will be done in the background."
|
||||
break
|
||||
|
||||
case "xepg.replace.missing.images":
|
||||
text = "If the poster in the XMLTV program is missing, the channel logo will be used."
|
||||
break
|
||||
|
||||
case "xepg.missing.epg.mode":
|
||||
text = "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved."
|
||||
break
|
||||
|
||||
case "udpxy":
|
||||
text = "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service."
|
||||
case "select":
|
||||
var text:string[] = field.options.map(function(option) { return option.text })
|
||||
var values:string[] = field.options.map(function(option) { return option.value })
|
||||
control = content.createSelect(text, values, data, field.key)
|
||||
break
|
||||
|
||||
default:
|
||||
text = ""
|
||||
// "text", "password", "number": lists (the update schedule) render comma separated.
|
||||
var value:any = data
|
||||
if (Array.isArray(data)) {
|
||||
value = data.join(",")
|
||||
}
|
||||
control = content.createInput(field.type, field.key, value)
|
||||
if (field.placeholder != undefined) {
|
||||
control.setAttribute("placeholder", field.placeholder)
|
||||
}
|
||||
if (field.autocomplete != undefined) {
|
||||
control.setAttribute("autocomplete", field.autocomplete)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
markChangedOnChange(control)
|
||||
return control
|
||||
}
|
||||
|
||||
createSettings(settingsKey:string):any {
|
||||
var setting = document.createElement("TR")
|
||||
var field:SettingsField = getSettingsField(settingsKey)
|
||||
if (field == undefined) {
|
||||
return setting
|
||||
}
|
||||
|
||||
var data = SERVER["settings"][settingsKey]
|
||||
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.textContent = field.title + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
tdRight.appendChild(this.createControl(field, data))
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
return setting
|
||||
}
|
||||
|
||||
createDescription(settingsKey:string):any {
|
||||
var description = document.createElement("TR")
|
||||
var field:SettingsField = getSettingsField(settingsKey)
|
||||
var text:string = ""
|
||||
if (field != undefined) {
|
||||
text = field.description
|
||||
}
|
||||
|
||||
var tdLeft = document.createElement("TD")
|
||||
@@ -577,7 +205,6 @@ class SettingsCategory {
|
||||
description.appendChild(tdRight)
|
||||
|
||||
return description
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -669,6 +296,7 @@ function saveSettings() {
|
||||
break
|
||||
|
||||
case "text":
|
||||
case "password":
|
||||
name = (settings[i] as HTMLInputElement).name
|
||||
value = (settings[i] as HTMLInputElement).value
|
||||
|
||||
|
||||
+10
-7
@@ -11,12 +11,15 @@
|
||||
"removeComments": false
|
||||
},
|
||||
"files": [
|
||||
"network_ts.ts",
|
||||
"menu_ts.ts",
|
||||
"settings_ts.ts",
|
||||
"logs_ts.ts",
|
||||
"base_ts.ts",
|
||||
"configuration_ts.ts",
|
||||
"authentication_ts.ts"
|
||||
"network.ts",
|
||||
"menu.ts",
|
||||
"content.ts",
|
||||
"popup.ts",
|
||||
"xmltv.ts",
|
||||
"settings.ts",
|
||||
"logs.ts",
|
||||
"base.ts",
|
||||
"configuration.ts",
|
||||
"authentication.ts"
|
||||
]
|
||||
}
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
class XMLTVFile {
|
||||
File:string
|
||||
|
||||
getFiles(set:string):any {
|
||||
var fileIDs:string[] = getObjKeys(SERVER["xepg"]["xmltvMap"])
|
||||
var values = new Array("-");
|
||||
var text = new Array("-");
|
||||
|
||||
for (let i = 0; i < fileIDs.length; i++) {
|
||||
if (fileIDs[i] != "xTeVe Dummy") {
|
||||
values.push(getValueFromProviderFile(fileIDs[i], "xmltv", "file.xteve"))
|
||||
text.push(getValueFromProviderFile(fileIDs[i], "xmltv", "name"))
|
||||
} else {
|
||||
values.push(fileIDs[i])
|
||||
text.push(fileIDs[i])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var select = document.createElement("SELECT")
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
var option = document.createElement("OPTION")
|
||||
option.setAttribute("value", values[i])
|
||||
option.innerText = text[i]
|
||||
select.appendChild(option)
|
||||
}
|
||||
|
||||
if(set != "") {
|
||||
(select as HTMLSelectElement).value = set
|
||||
}
|
||||
|
||||
return select
|
||||
}
|
||||
|
||||
getPrograms(file:string, set:string):any {
|
||||
//var fileIDs:string[] = getObjKeys(SERVER["xepg"]["xmltvMap"])
|
||||
var values = getObjKeys(SERVER["xepg"]["xmltvMap"][file]);
|
||||
var text = new Array()
|
||||
var displayName:string
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (SERVER["xepg"]["xmltvMap"][file][values[i]].hasOwnProperty('display-name') == true) {
|
||||
displayName = SERVER["xepg"]["xmltvMap"][file][values[i]]["display-name"];
|
||||
} else {
|
||||
displayName = "-"
|
||||
}
|
||||
|
||||
text[i] = displayName + " (" + values[i] + ")";
|
||||
}
|
||||
|
||||
text.unshift("-");
|
||||
values.unshift("-");
|
||||
|
||||
var select = document.createElement("SELECT")
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
var option = document.createElement("OPTION")
|
||||
option.setAttribute("value", values[i])
|
||||
option.innerText = text[i]
|
||||
select.appendChild(option)
|
||||
}
|
||||
|
||||
if(set != "") {
|
||||
(select as HTMLSelectElement).value = set
|
||||
}
|
||||
|
||||
if ((select as HTMLSelectElement).value != set) {
|
||||
(select as HTMLSelectElement).value = "-"
|
||||
}
|
||||
|
||||
return select
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function getValueFromProviderFile(file:string, fileType, key) {
|
||||
|
||||
if (file == "xTeVe Dummy") {
|
||||
return file
|
||||
}
|
||||
|
||||
var fileID:string
|
||||
var indicator = file.charAt(0)
|
||||
|
||||
switch (indicator) {
|
||||
case "M":
|
||||
fileType = "m3u"
|
||||
fileID = file
|
||||
break;
|
||||
|
||||
case "H":
|
||||
fileType = "hdhr"
|
||||
fileID = file
|
||||
break;
|
||||
|
||||
case "X":
|
||||
fileType = "xmltv"
|
||||
fileID = file.substring(0, file.lastIndexOf('.'))
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (SERVER["settings"]["files"][fileType].hasOwnProperty(fileID) == true) {
|
||||
var data = SERVER["settings"]["files"][fileType][fileID];
|
||||
return data[key]
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
function setXmltvChannel(id, element) {
|
||||
|
||||
var xmltv:XMLTVFile = new XMLTVFile()
|
||||
var xmlFile = element.value
|
||||
|
||||
var tvgId:string = SERVER["xepg"]["epgMapping"][id]["tvg-id"]
|
||||
var td = document.getElementById("popup-mapping").parentElement
|
||||
td.innerHTML = ""
|
||||
|
||||
var select = xmltv.getPrograms(element.value, tvgId)
|
||||
select.setAttribute("name", "x-mapping")
|
||||
select.setAttribute("id", "popup-mapping")
|
||||
markChangedOnChange(select, function(element) { checkXmltvChannel(id, element, xmlFile) })
|
||||
select.className = "changed"
|
||||
sortSelect(select)
|
||||
td.appendChild(select);
|
||||
|
||||
checkXmltvChannel(id, select, xmlFile)
|
||||
}
|
||||
|
||||
function checkXmltvChannel(id:string, element:any, xmlFile) {
|
||||
|
||||
var value = (element as HTMLSelectElement).value
|
||||
var bool:boolean
|
||||
var checkbox = document.getElementById('active')
|
||||
var channel:any = SERVER["xepg"]["epgMapping"][id]
|
||||
var updateLogo:boolean
|
||||
|
||||
|
||||
if (value == "-") {
|
||||
bool = false
|
||||
} else {
|
||||
bool = true
|
||||
}
|
||||
|
||||
(checkbox as HTMLInputElement).checked = bool
|
||||
checkbox.className = "changed"
|
||||
console.log(xmlFile);
|
||||
|
||||
// Kanallogo aktualisieren
|
||||
/*
|
||||
updateLogo = (document.getElementById("update-icon") as HTMLInputElement).checked
|
||||
console.log(updateLogo);
|
||||
*/
|
||||
|
||||
if(xmlFile != "xTeVe Dummy" && bool == true) {
|
||||
|
||||
//(document.getElementById("update-icon") as HTMLInputElement).checked = true;
|
||||
//(document.getElementById("update-icon") as HTMLInputElement).className = "changed";
|
||||
|
||||
console.log("ID", id)
|
||||
changeChannelLogo(id)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (xmlFile == "xTeVe Dummy") {
|
||||
(document.getElementById("update-icon") as HTMLInputElement).checked = false;
|
||||
(document.getElementById("update-icon") as HTMLInputElement).className = "changed";
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function changeChannelLogo(id:string) {
|
||||
|
||||
var updateLogo:boolean
|
||||
var channel:any = SERVER["xepg"]["epgMapping"][id]
|
||||
|
||||
var f = (document.getElementById("popup-xmltv") as HTMLSelectElement);
|
||||
var xmltvFile = f.options[f.selectedIndex].value;
|
||||
|
||||
var m = (document.getElementById("popup-mapping") as HTMLSelectElement);
|
||||
var xMapping = m.options[m.selectedIndex].value;
|
||||
|
||||
var xmltvLogo = SERVER["xepg"]["xmltvMap"][xmltvFile][xMapping]["icon"]
|
||||
updateLogo = (document.getElementById("update-icon") as HTMLInputElement).checked
|
||||
|
||||
if (updateLogo == true && xmltvFile != "xTeVe Dummy") {
|
||||
|
||||
if (SERVER["xepg"]["xmltvMap"][xmltvFile].hasOwnProperty(xMapping)) {
|
||||
var logo = xmltvLogo
|
||||
} else {
|
||||
logo = channel["tvg-logo"]
|
||||
}
|
||||
|
||||
var logoInput = (document.getElementById("channel-icon") as HTMLInputElement);
|
||||
logoInput.value = logo
|
||||
if (BULK_EDIT == false) {
|
||||
logoInput.className = "changed"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user