From ce844e0574de223ab369d0132208af28f6e40d42 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 23 Sep 2020 20:14:16 +0100 Subject: [PATCH] add ts-debug option to makefile for including typescript and sourcemaps --- .gitignore | 1 + Makefile | 4 + data/static/ts/accounts.ts | 348 ++++++++++++++++++++++++++++++++ data/static/ts/admin.ts | 279 ++++++++++++++++++++++++++ data/static/ts/animation.ts | 79 ++++++++ data/static/ts/bs4.ts | 36 ++++ data/static/ts/bs5.ts | 34 ++++ data/static/ts/invites.ts | 378 +++++++++++++++++++++++++++++++++++ data/static/ts/ombi.ts | 81 ++++++++ data/static/ts/serialize.ts | 35 ++++ data/static/ts/settings.ts | 344 +++++++++++++++++++++++++++++++ data/static/ts/tsconfig.json | 8 + views.go | 4 +- 13 files changed, 1629 insertions(+), 2 deletions(-) create mode 100644 data/static/ts/accounts.ts create mode 100644 data/static/ts/admin.ts create mode 100644 data/static/ts/animation.ts create mode 100644 data/static/ts/bs4.ts create mode 100644 data/static/ts/bs5.ts create mode 100644 data/static/ts/invites.ts create mode 100644 data/static/ts/ombi.ts create mode 100644 data/static/ts/serialize.ts create mode 100644 data/static/ts/settings.ts create mode 100644 data/static/ts/tsconfig.json diff --git a/.gitignore b/.gitignore index 6dafb29..3255bad 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ scss/bs4/*.css* scss/bs5/*.css* data/static/*.css data/static/*.js +data/static/*.js.map !data/static/setup.js data/config-base.json data/config-default.ini diff --git a/Makefile b/Makefile index 90ba951..159e91d 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,10 @@ typescript: $(info Compiling typescript) -npx tsc -p ts/ +ts-debug: + -npx tsc -p ts/ --sourceMap + cp -r ts data/static/ + version: python3 version.py auto version.go diff --git a/data/static/ts/accounts.ts b/data/static/ts/accounts.ts new file mode 100644 index 0000000..4d171cb --- /dev/null +++ b/data/static/ts/accounts.ts @@ -0,0 +1,348 @@ +const checkCheckboxes = (): void => { + const defaultsButton = document.getElementById('accountsTabSetDefaults'); + const deleteButton = document.getElementById('accountsTabDelete'); + const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); + let checked = checkboxes.length; + if (checked == 0) { + Unfocus(defaultsButton); + Unfocus(deleteButton); + } else { + Focus(defaultsButton); + Focus(deleteButton); + if (checked == 1) { + deleteButton.textContent = 'Delete User'; + } else { + deleteButton.textContent = 'Delete Users'; + } + } +} + +const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email); + +function changeEmail(icon: HTMLElement, id: string): void { + const iconContent = icon.outerHTML; + icon.setAttribute('class', ''); + const entry = icon.nextElementSibling as HTMLInputElement; + const ogEmail = entry.value; + entry.readOnly = false; + entry.classList.remove('form-control-plaintext'); + entry.classList.add('form-control'); + if (ogEmail == "") { + entry.placeholder = 'Address'; + } + const tick = createEl(` + + `); + tick.onclick = (): void => { + const newEmail = entry.value; + if (!validateEmail(newEmail) || newEmail == ogEmail) { + return; + } + cross.remove(); + const spinner = createEl(` +
+ Saving... +
+ `); + tick.replaceWith(spinner); + let send = {}; + send[id] = newEmail; + _post("/modifyEmails", send, function (): void { + if (this.readyState == 4) { + if (this.status == 200 || this.status == 204) { + entry.nextElementSibling.remove(); + } else { + entry.value = ogEmail; + } + } + }); + icon.outerHTML = iconContent; + entry.readOnly = true; + entry.classList.remove('form-control'); + entry.classList.add('form-control-plaintext'); + entry.placeholder = ''; + }; + const cross = createEl(` + + `); + cross.onclick = (): void => { + tick.remove(); + cross.remove(); + icon.outerHTML = iconContent; + entry.readOnly = true; + entry.classList.remove('form-control'); + entry.classList.add('form-control-plaintext'); + entry.placeholder = ''; + entry.value = ogEmail; + }; + icon.parentNode.appendChild(tick); + icon.parentNode.appendChild(cross); +}; + +var jfUsers: Array; + +function populateUsers(): void { + const acList = document.getElementById('accountsList'); + acList.innerHTML = ` +
+ Getting Users... + +
+ `; + Unfocus(acList.parentNode.querySelector('thead')); + const accountsList = document.createElement('tbody'); + accountsList.id = 'accountsList'; + const generateEmail = (id: string, name: string, email: string): string => { + let entry: HTMLDivElement = document.createElement('div'); + entry.id = 'email_' + id; + let emailValue: string = email; + if (emailValue == undefined) { + emailValue = ""; + } + entry.innerHTML = ` + + + `; + return entry.outerHTML; + }; + const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => { + let isAdmin = "No"; + if (admin) { + isAdmin = "Yes"; + } + let fci = "form-check-input"; + if (bsVersion != 5) { + fci = ""; + } + return ` + + ${username} + ${generateEmail(id, name, email)} + ${lastActive} + ${isAdmin} + `; + }; + + _get("/getUsers", null, function (): void { + if (this.readyState == 4 && this.status == 200) { + jfUsers = this.response['users']; + for (const user of jfUsers) { + let tr = document.createElement('tr'); + tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']); + accountsList.appendChild(tr); + } + Focus(acList.parentNode.querySelector('thead')); + acList.replaceWith(accountsList); + } + }); +} + +function populateRadios(): void { + const radioList = document.getElementById('defaultUserRadios'); + radioList.textContent = ''; + let first = true; + for (const i in jfUsers) { + const user = jfUsers[i]; + const radio = document.createElement('div'); + radio.classList.add('form-check'); + let checked = ''; + if (first) { + checked = 'checked'; + first = false; + } + radio.innerHTML = ` + + `; + radioList.appendChild(radio); + } +} + +(document.getElementById('selectAll')).onclick = function (): void { + const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); + for (let i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = (this).checked; + } + checkCheckboxes(); +}; + +(document.getElementById('deleteModalNotify')).onclick = function (): void { + const textbox: HTMLElement = document.getElementById('deleteModalReasonBox'); + if ((this).checked) { + Focus(textbox); + } else { + Unfocus(textbox); + } +}; + +(document.getElementById('accountsTabDelete')).onclick = function (): void { + const deleteButton = this as HTMLButtonElement; + const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); + let selected: Array = new Array(checkboxes.length); + for (let i = 0; i < checkboxes.length; i++) { + selected[i] = checkboxes[i].id.replace("select_", ""); + } + let title = " user"; + let msg = "Notify user"; + if (selected.length > 1) { + title += "s"; + msg += "s"; + } + title = `Delete ${selected.length} ${title}`; + msg += " of account deletion"; + + document.getElementById('deleteModalTitle').textContent = title; + const dmNotify = document.getElementById('deleteModalNotify') as HTMLInputElement; + dmNotify.checked = false; + document.getElementById('deleteModalNotifyLabel').textContent = msg; + const dmReason = document.getElementById('deleteModalReason') as HTMLTextAreaElement; + dmReason.value = ''; + Unfocus(document.getElementById('deleteModalReasonBox')); + const dmSend = document.getElementById('deleteModalSend') as HTMLButtonElement; + dmSend.textContent = 'Delete'; + dmSend.onclick = function (): void { + const button = this as HTMLButtonElement; + const send = { + 'users': selected, + 'notify': dmNotify.checked, + 'reason': dmReason.value + }; + _post("/deleteUser", send, function (): void { + if (this.readyState == 4) { + if (this.status == 500) { + if ("error" in this.reponse) { + button.textContent = 'Failed'; + } else { + button.textContent = 'Partial fail (check console)'; + console.log(this.response); + } + setTimeout((): void => { + Unfocus(deleteButton); + deleteModal.hide(); + }, 4000); + } else { + Unfocus(deleteButton); + deleteModal.hide() + } + populateUsers(); + checkCheckboxes(); + } + }); + }; + deleteModal.show(); +}; + +(document.getElementById('selectAll')).checked = false; + +(document.getElementById('accountsTabSetDefaults')).onclick = function (): void { + const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); + let userIDs: Array = new Array(checkboxes.length); + for (let i = 0; i < checkboxes.length; i++){ + userIDs[i] = checkboxes[i].id.replace("select_", ""); + } + if (userIDs.length == 0) { + return; + } + populateRadios(); + let userString = 'user'; + if (userIDs.length > 1) { + userString += "s"; + } + populateProfiles(true); + const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement; + profileSelect.textContent = ''; + for (let i = 0; i < availableProfiles.length; i++) { + profileSelect.innerHTML += ` + + `; + } + document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`; + document.getElementById('userDefaultsDescription').textContent = ` + Apply settings from an existing profile or source settings from a user. + `; + document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`; + Focus(document.getElementById('defaultsSourceSection')); + (document.getElementById('defaultsSource')).value = 'profile'; + Focus(document.getElementById('profileSelectBox')); + Unfocus(document.getElementById('defaultUserRadios')); + Unfocus(document.getElementById('newProfileBox')); + document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs); + userDefaultsModal.show(); +}; + +(document.getElementById('defaultsSource')).addEventListener('change', function (): void { + const radios = document.getElementById('defaultUserRadios'); + const profileBox = document.getElementById('profileSelectBox'); + if (this.value == 'profile') { + Unfocus(radios); + Focus(profileBox); + } else { + Unfocus(profileBox); + Focus(radios); + } +}); + +(document.getElementById('newUserCreate')).onclick = function (): void { + const button = this as HTMLButtonElement; + const ogText = button.textContent; + button.innerHTML = ` + Creating... + `; + const email: string = (document.getElementById('newUserEmail')).value; + var username: string = email; + if (document.getElementById('newUserName') != null) { + username = (document.getElementById('newUserName')).value; + } + const password: string = (document.getElementById('newUserPassword')).value; + if (!validateEmail(email) && email != "") { + return; + } + const send = { + 'username': username, + 'password': password, + 'email': email + }; + _post("/newUserAdmin", send, function (): void { + if (this.readyState == 4) { + rmAttr(button, 'btn-primary'); + if (this.status == 200) { + addAttr(button, 'btn-success'); + button.textContent = 'Success'; + setTimeout((): void => { + rmAttr(button, 'btn-success'); + addAttr(button, 'btn-primary'); + button.textContent = ogText; + newUserModal.hide(); + }, 1000); + populateUsers(); + } else { + addAttr(button, 'btn-danger'); + if ("error" in this.response) { + button.textContent = this.response["error"]; + } else { + button.textContent = 'Failed'; + } + setTimeout((): void => { + rmAttr(button, 'btn-danger'); + addAttr(button, 'btn-primary'); + button.textContent = ogText; + }, 2000); + populateUsers(); + } + } + }); +}; + +(document.getElementById('accountsTabAddUser')).onclick = function (): void { + (document.getElementById('newUserEmail')).value = ''; + (document.getElementById('newUserPassword')).value = ''; + if (document.getElementById('newUserName') != null) { + (document.getElementById('newUserName')).value = ''; + } + newUserModal.show(); +}; + + + + + + diff --git a/data/static/ts/admin.ts b/data/static/ts/admin.ts new file mode 100644 index 0000000..cc5cad5 --- /dev/null +++ b/data/static/ts/admin.ts @@ -0,0 +1,279 @@ +interface Window { + token: string; +} + +// Set in admin.html +var cssFile: string; + +const _post = (url: string, data: Object, onreadystatechange: () => void): void => { + let req = new XMLHttpRequest(); + req.open("POST", url, true); + req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); + req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.onreadystatechange = onreadystatechange; + req.send(JSON.stringify(data)); +}; + +const _get = (url: string, data: Object, onreadystatechange: () => void): void => { + let req = new XMLHttpRequest(); + req.open("GET", url, true); + req.responseType = 'json'; + req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); + req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.onreadystatechange = onreadystatechange; + req.send(JSON.stringify(data)); +}; + +const rmAttr = (el: HTMLElement, attr: string): void => { + if (el.classList.contains(attr)) { + el.classList.remove(attr); + } +}; +const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr); + +const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused'); +const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused'); + +interface TabSwitcher { + els: Array; + tabButtons: Array; + focus: (el: number) => void; + invites: () => void; + accounts: () => void; + settings: () => void; +} + +const tabs: TabSwitcher = { + els: [document.getElementById('invitesTab') as HTMLDivElement, document.getElementById('accountsTab') as HTMLDivElement, document.getElementById('settingsTab') as HTMLDivElement], + tabButtons: [document.getElementById('invitesTabButton') as HTMLAnchorElement, document.getElementById('accountsTabButton') as HTMLAnchorElement, document.getElementById('settingsTabButton') as HTMLAnchorElement], + focus: (el: number): void => { + for (let i = 0; i < tabs.els.length; i++) { + if (i == el) { + Focus(tabs.els[i]); + addAttr(tabs.tabButtons[i], "active"); + } else { + Unfocus(tabs.els[i]); + rmAttr(tabs.tabButtons[i], "active"); + } + } + }, + invites: (): void => tabs.focus(0), + accounts: (): void => { + populateUsers(); + (document.getElementById('selectAll') as HTMLInputElement).checked = false; + checkCheckboxes(); + tabs.focus(1); + }, + settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => { + triggerTooltips(); + showSetting("ui"); + tabs.focus(2); + }) +}; + +// for (let i = 0; i < tabs.els.length; i++) { +// tabs.tabButtons[i].onclick = (): void => tabs.focus(i); +// } +tabs.tabButtons[0].onclick = tabs.invites; +tabs.tabButtons[1].onclick = tabs.accounts; +tabs.tabButtons[2].onclick = tabs.settings; + + +tabs.invites(); + +// Predefined colors for the theme button. +var buttonColor: string = "custom"; +if (cssFile.includes("jf")) { + buttonColor = "rgb(255,255,255)"; +} else if (cssFile == ("bs" + bsVersion + ".css")) { + buttonColor = "rgb(16,16,16)"; +} + +if (buttonColor != "custom") { + const switchButton = document.createElement('button') as HTMLButtonElement; + switchButton.classList.add('btn', 'btn-secondary'); + switchButton.innerHTML = ` + Theme + + `; + switchButton.onclick = (): void => toggleCSS(document.getElementById('fakeButton')); + document.getElementById('headerButtons').appendChild(switchButton); +} + +var loginModal = createModal('login'); +var userDefaultsModal = createModal('userDefaults'); +var usersModal = createModal('users'); +var restartModal = createModal('restartModal'); +var refreshModal = createModal('refreshModal'); +var aboutModal = createModal('aboutModal'); +var deleteModal = createModal('deleteModal'); +var newUserModal = createModal('newUserModal'); + +var availableProfiles: Array; + +window["token"] = ""; + +function toClipboard(str: string): void { + const el = document.createElement('textarea') as HTMLTextAreaElement; + el.value = str; + el.readOnly = true; + el.style.position = "absolute"; + el.style.left = "-9999px"; + document.body.appendChild(el); + const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); + if (selected) { + document.getSelection().removeAllRanges(); + document.getSelection().addRange(selected); + } +} + +function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void { + const req = new XMLHttpRequest(); + req.responseType = 'json'; + req.open("GET", "/getToken", true); + req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password)); + req.onreadystatechange = function (): void { + if (this.readyState == 4) { + if (this.status != 200) { + let errorMsg = this.response["error"]; + if (!errorMsg) { + errorMsg = "Unknown error"; + } + if (modal) { + button.disabled = false; + button.textContent = errorMsg; + addAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + setTimeout((): void => { + addAttr(button, "btn-primary"); + rmAttr(button, "btn-danger"); + button.textContent = "Login"; + }, 4000); + } else { + loginModal.show(); + } + } else { + const data = this.response; + window.token = data["token"]; + generateInvites(); + setInterval((): void => generateInvites(), 60 * 1000); + addOptions(30, document.getElementById('days') as HTMLSelectElement); + addOptions(24, document.getElementById('hours') as HTMLSelectElement); + const minutes = document.getElementById('minutes') as HTMLSelectElement; + addOptions(59, minutes); + minutes.value = "30"; + checkDuration(); + if (modal) { + loginModal.hide(); + } + Focus(document.getElementById('logoutButton')); + } + if (run) { + run(+this.status); + } + } + }; + req.send(); +} + +function createEl(html: string): HTMLElement { + let div = document.createElement('div') as HTMLDivElement; + div.innerHTML = html; + return div.firstElementChild as HTMLElement; +} + +(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean { + window.token = ""; + const details = serializeForm('loginForm'); + const button = document.getElementById('loginSubmit') as HTMLButtonElement; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-danger"); + button.disabled = true; + button.innerHTML = ` + + Loading...`; + login(details["username"], details["password"], true, button); + return false; +}; + +function storeDefaults(users: string | Array): void { + // not sure if this does anything, but w/e + this.disabled = true; + this.innerHTML = + '' + + 'Loading...'; + const button = document.getElementById('storeDefaults') as HTMLButtonElement; + let data = { "homescreen": false }; + if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') { + data["from"] = "profile"; + data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value; + } else { + const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement + let id = radio.id.replace("default_", ""); + data["from"] = "user"; + data["id"] = id; + } + if (users != "all") { + data["apply_to"] = users; + } + if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) { + data["homescreen"] = true; + } + _post("/applySettings", data, function (): void { + if (this.readyState == 4) { + if (this.status == 200 || this.status == 204) { + button.textContent = "Success"; + addAttr(button, "btn-success"); + rmAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + button.disabled = false; + setTimeout((): void => { + button.textContent = "Submit"; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-success"); + button.disabled = false; + userDefaultsModal.hide(); + }, 1000); + } else { + if ("error" in this.response) { + button.textContent = this.response["error"]; + } else if (("policy" in this.response) || ("homescreen" in this.response)) { + button.textContent = "Failed (check console)"; + } else { + button.textContent = "Failed"; + } + addAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + setTimeout((): void => { + button.textContent = "Submit"; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-danger"); + button.disabled = false; + }, 1000); + } + } + }); +} + +generateInvites(true); + +login("", "", false, null, (status: number): void => { + if (!(status == 200 || status == 204)) { + loginModal.show(); + } +}); + +(document.getElementById('logoutButton') as HTMLButtonElement).onclick = function (): void { + _post("/logout", null, function (): boolean { + if (this.readyState == 4 && this.status == 200) { + window.token = ""; + location.reload(); + return false; + } + }); +}; + + diff --git a/data/static/ts/animation.ts b/data/static/ts/animation.ts new file mode 100644 index 0000000..8b5b8c5 --- /dev/null +++ b/data/static/ts/animation.ts @@ -0,0 +1,79 @@ +// Used for animation on theme change +const whichTransitionEvent = (): string => { + const el = document.createElement('fakeElement'); + const transitions = { + 'transition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'MozTransition': 'transitionend', + 'WebkitTransition': 'webkitTransitionEnd' + }; + for (const t in transitions) { + if (el.style[t] !== undefined) { + return transitions[t]; + } + } + return ''; +}; + +var transitionEndEvent = whichTransitionEvent(); + +// Toggles between light and dark themes +const _toggleCSS = (): void => { + const els: NodeListOf = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]'); + let cssEl = 0; + let remove = false; + if (els.length != 1) { + cssEl = 1; + remove = true + } + let href: string = "bs" + bsVersion; + if (!els[cssEl].href.includes(href + "-jf")) { + href += "-jf"; + } + href += ".css"; + let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement; + newEl.href = href; + els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling); + if (remove) { + els[0].remove(); + } + document.cookie = "css=" + href; +} + +// Toggles between light and dark themes, but runs animation if window small enough. +var buttonWidth = 0; +const toggleCSS = (el: HTMLElement): void => { + const switchToColor = window.getComputedStyle(document.body, null).backgroundColor; + // Max page width for animation to take place + let maxWidth = 1500; + if (window.innerWidth < maxWidth) { + // Calculate minimum radius to cover screen + const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)); + const currentRadius = el.getBoundingClientRect().width / 2; + const scale = radius / currentRadius; + buttonWidth = +window.getComputedStyle(el, null).width; + document.body.classList.remove('smooth-transition'); + el.style.transform = `scale(${scale})`; + el.style.color = switchToColor; + el.addEventListener(transitionEndEvent, function (): void { + if (this.style.transform.length != 0) { + _toggleCSS(); + this.style.removeProperty('transform'); + document.body.classList.add('smooth-transition'); + } + }, false); + } else { + _toggleCSS(); + el.style.color = switchToColor; + } +}; + +const rotateButton = (el: HTMLElement): void => { + if (el.classList.contains("rotated")) { + rmAttr(el, "rotated") + addAttr(el, "not-rotated"); + } else { + rmAttr(el, "not-rotated"); + addAttr(el, "rotated"); + } +}; diff --git a/data/static/ts/bs4.ts b/data/static/ts/bs4.ts new file mode 100644 index 0000000..61131e0 --- /dev/null +++ b/data/static/ts/bs4.ts @@ -0,0 +1,36 @@ +var bsVersion = 4; + +const send_to_addess_enabled = document.getElementById('send_to_addess_enabled'); +if (send_to_addess_enabled) { + send_to_addess_enabled.classList.remove("form-check-input"); +} +const multiUseEnabled = document.getElementById('multiUseEnabled'); +if (multiUseEnabled) { + multiUseEnabled.classList.remove("form-check-input"); +} + +function createModal(id: string, find?: boolean): any { + $(`#${id}`).on("shown.bs.modal", (): void => document.body.classList.add("modal-open")); + return { + show: function (): any { + const temp = ($(`#${id}`) as any).modal("show"); + return temp; + }, + hide: function (): any { + return ($(`#${id}`) as any).modal("hide"); + } + }; +} + +function triggerTooltips(): void { + const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]')); + for (const i in checkboxes) { + checkboxes[i].click(); + checkboxes[i].click(); + } + const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]')); + tooltips.map((el: HTMLAnchorElement): any => { + return ($(el) as any).tooltip(); + }); +} + diff --git a/data/static/ts/bs5.ts b/data/static/ts/bs5.ts new file mode 100644 index 0000000..5dc69cd --- /dev/null +++ b/data/static/ts/bs5.ts @@ -0,0 +1,34 @@ +declare var bootstrap: any; + +var bsVersion = 5; + +function createModal(id: string, find?: boolean): any { + let modal: any; + if (find) { + modal = bootstrap.Modal.getInstance(document.getElementById(id)); + } else { + modal = new bootstrap.Modal(document.getElementById(id)); + } + document.getElementById(id).addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open")); + return { + modal: modal, + show: function (): any { + const temp = this.modal.show(); + return temp; + }, + hide: function (): any { return this.modal.hide(); } + }; +} + +function triggerTooltips(): void { + const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]')); + for (const i in checkboxes) { + checkboxes[i].click(); + checkboxes[i].click(); + } + const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]')); + tooltips.map((el: HTMLAnchorElement): any => { + return new bootstrap.Tooltip(el); + }); +} + diff --git a/data/static/ts/invites.ts b/data/static/ts/invites.ts new file mode 100644 index 0000000..0b2b16c --- /dev/null +++ b/data/static/ts/invites.ts @@ -0,0 +1,378 @@ +// Actually defined by templating in admin.html, this is just to avoid errors from tsc. +var notifications_enabled: any; + +interface Invite { + code?: string; + expiresIn?: string; + empty: boolean; + remainingUses?: string; + email?: string; + usedBy?: Array>; + created?: string; + notifyExpiry?: boolean; + notifyCreation?: boolean; + profile?: string; +} + +const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; } + +function parseInvite(invite: Object): Invite { + let inv: Invite = { code: invite["code"], empty: false, }; + if (invite["email"]) { + inv.email = invite["email"]; + } + let time = "" + const f = ["days", "hours", "minutes"]; + for (const i in f) { + if (invite[f[i]] != 0) { + time += `${invite[f[i]]}${f[i][0]} `; + } + } + inv.expiresIn = `Expires in ${time.slice(0, -1)}`; + if (invite["no-limit"]) { + inv.remainingUses = "∞"; + } else if ("remaining-uses" in invite) { + inv.remainingUses = invite["remaining-uses"]; + } + if ("used-by" in invite) { + inv.usedBy = invite["used-by"]; + } + if ("created" in invite) { + inv.created = invite["created"]; + } + if ("notify-expiry" in invite) { + inv.notifyExpiry = invite["notify-expiry"]; + } + if ("notify-creation" in invite) { + inv.notifyCreation = invite["notify-creation"]; + } + if ("profile" in invite) { + inv.profile = invite["profile"]; + } + return inv; +} + +function setNotify(el: HTMLElement): void { + let send = {}; + let code: string; + let notifyType: string; + if (el.id.includes("Expiry")) { + code = el.id.replace("_notifyExpiry", ""); + notifyType = "notify-expiry"; + } else if (el.id.includes("Creation")) { + code = el.id.replace("_notifyCreation", ""); + notifyType = "notify-creation"; + } + send[code] = {}; + send[code][notifyType] = (el as HTMLInputElement).checked; + _post("/setNotify", send, function (): void { + if (this.readyState == 4 && this.status != 200) { + (el as HTMLInputElement).checked = !(el as HTMLInputElement).checked; + } + }); +} + +function genUsedBy(usedBy: Array>): string { + let uB = ""; + if (usedBy && usedBy.length != 0) { + uB = ` +
    +
  • Users created:
  • + `; + for (const i in usedBy) { + uB += ` +
  • +
    ${usedBy[i][0]}
    +
    ${usedBy[i][1]}
    +
  • + `; + } + uB += `
` + } + return uB; +} + +function addItem(invite: Invite): void { + const links = document.getElementById('invites'); + const container = document.createElement('div') as HTMLDivElement; + container.id = invite.code; + const item = document.createElement('div') as HTMLDivElement; + item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block'); + let link = ""; + let innerHTML = `None`; + if (invite.empty) { + item.innerHTML = ` +
+ ${innerHTML} +
+ `; + container.appendChild(item); + links.appendChild(container); + return; + } + link = window.location.href.split('#')[0] + "invite/" + invite.code; + innerHTML = ` +
+ ${invite.code.replace(/-/g, '-')} + + `; + if (invite.email) { + let email = invite.email; + if (!invite.email.includes("Failed to send to")) { + email = `Sent to ${email}`; + } + innerHTML += ` + ${email} + `; + } + innerHTML += ` +
+
+ ${invite.expiresIn} +
+ + +
+
+ `; + + item.innerHTML = innerHTML; + container.appendChild(item); + + let profiles = ` + + `; + + let dateCreated: string; + if (invite.created) { + dateCreated = `
  • Created: ${invite.created}
  • `; + } + + let middle: string; + if (notifications_enabled) { + middle = ` +
    +
      + Notify on: +
    • + + +
    • +
    • + + +
    • +
    +
    + `; + } + + let right: string = genUsedBy(invite.usedBy) + + const dropdown = document.createElement('div') as HTMLDivElement; + dropdown.id = `${CSS.escape(invite.code)}_collapse`; + dropdown.classList.add("collapse"); + dropdown.innerHTML = ` +
    +
    +
      +
    • + ${profiles} +
    • + ${dateCreated} +
    • Remaining uses: ${invite.remainingUses}
    • +
    +
    + ${middle} +
    + ${right} +
    +
    + `; + + container.appendChild(dropdown); + links.appendChild(container); +} + +function updateInvite(invite: Invite): void { + document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn; + const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses"); + if (remainingUses) { + remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`; + } + document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy); +} + +// delete invite from DOM +const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove(); + +// delete invite from jfa-go +const deleteInvite = (code: string): void => _post("/deleteInvite", { "code": code }, function (): void { + if (this.readyState == 4) { + generateInvites(); + } +}); + +function generateInvites(empty?: boolean): void { + if (empty) { + document.getElementById('invites').textContent = ''; + addItem(emptyInvite()); + return; + } + _get("/getInvites", null, function (): void { + if (this.readyState == 4) { + let data = this.response; + availableProfiles = data['profiles']; + const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement; + let innerHTML = ""; + for (let i = 0; i < availableProfiles.length; i++) { + const profile = availableProfiles[i]; + innerHTML += ` + + `; + } + innerHTML += ` + + `; + Profiles.innerHTML = innerHTML; + if (data['invites'] == null || data['invites'].length == 0) { + document.getElementById('invites').textContent = ''; + addItem(emptyInvite()); + return; + } + let items = document.getElementById('invites').children; + for (const i in data['invites']) { + let match = false; + const inv = parseInvite(data['invites'][i]); + for (const x in items) { + if (items[x].id == inv.code) { + match = true; + updateInvite(inv); + break; + } + } + if (!match) { + addItem(inv); + } + } + // second pass to check for expired invites + items = document.getElementById('invites').children; + for (let i = 0; i < items.length; i++) { + let exists = false; + for (const x in data['invites']) { + if (items[i].id == data['invites'][x]['code']) { + exists = true; + break; + } + } + if (!exists) { + hideInvite(items[i].id); + } + } + } + }); +} + +const addOptions = (length: number, el: HTMLSelectElement): void => { + for (let v = 0; v <= length; v++) { + const opt = document.createElement('option'); + opt.textContent = ""+v; + opt.value = ""+v; + el.appendChild(opt); + } + el.value = "0"; +}; + +function fixCheckboxes(): void { + const send_to_address: Array = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement]; + if (send_to_address[0] != null) { + send_to_address[0].disabled = !send_to_address[1].checked; + } + const multiUseEnabled = document.getElementById('multiUseEnabled') as HTMLInputElement; + const multiUseCount = document.getElementById('multiUseCount') as HTMLInputElement; + const noUseLimit = document.getElementById('noUseLimit') as HTMLInputElement; + multiUseCount.disabled = !multiUseEnabled.checked; + noUseLimit.checked = false; + noUseLimit.disabled = !multiUseEnabled.checked; +} + +fixCheckboxes(); + +(document.getElementById('inviteForm') as HTMLFormElement).onsubmit = function (): boolean { + const button = document.getElementById('generateSubmit') as HTMLButtonElement; + button.disabled = true; + button.innerHTML = ` + + Loading...`; + let send = serializeForm('inviteForm'); + send["remaining-uses"] = +send["remaining-uses"]; + if (!send['multiple-uses'] || send['no-limit']) { + delete send['remaining-uses']; + } + if (send["profile"] == "NoProfile") { + send["profile"] = ""; + } + const sendToAddress: any = document.getElementById('send_to_address'); + const sendToAddressEnabled: any = document.getElementById('send_to_address_enabled'); + if (sendToAddress && sendToAddressEnabled) { + send['email'] = send['send_to_address']; + delete send['send_to_address']; + delete send['send_to_address_enabled']; + } + console.log(send); + _post("/generateInvite", send, function (): void { + if (this.readyState == 4) { + button.textContent = 'Generate'; + button.disabled = false; + generateInvites(); + } + }); + return false; +}; + +triggerTooltips(); + +function setProfile(select: HTMLSelectElement): void { + if (!select.value) { + return; + } + let val = select.value; + if (select.value == "NoProfile") { + val = "" + } + const invite = select.id.replace("profile_", ""); + const send = { + "invite": invite, + "profile": val + }; + _post("/setProfile", send, function (): void { + if (this.readyState == 4 && this.status != 200) { + generateInvites(); + } + }); +} + +function checkDuration(): void { + const boxVals: Array = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value]; + const submit = document.getElementById('generateSubmit') as HTMLButtonElement; + if (boxVals.reduce((a: number, b: number): number => a + b) == 0) { + submit.disabled = true; + } else { + submit.disabled = false; + } +} + +const nE: Array = ["days", "hours", "minutes"]; +for (const i in nE) { + document.getElementById(nE[i]).addEventListener("change", checkDuration); +} diff --git a/data/static/ts/ombi.ts b/data/static/ts/ombi.ts new file mode 100644 index 0000000..792de69 --- /dev/null +++ b/data/static/ts/ombi.ts @@ -0,0 +1,81 @@ +const ombiDefaultsModal = createModal('ombiDefaults'); +(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void { + let button = this as HTMLButtonElement; + button.disabled = true; + const ogHTML = button.innerHTML; + button.innerHTML = + '' + + 'Loading...'; + _get("/getOmbiUsers", null, function (): void { + if (this.readyState == 4) { + if (this.status == 200) { + const users = this.response['users']; + const radioList = document.getElementById('ombiUserRadios'); + radioList.textContent = ''; + let first = true; + for (const i in users) { + const user = users[i]; + const radio = document.createElement('div') as HTMLDivElement; + radio.classList.add('form-check'); + let checked = ''; + if (first) { + checked = 'checked'; + first = false; + } + radio.innerHTML = ` + + + `; + radioList.appendChild(radio); + } + button.disabled = false; + button.innerHTML = ogHTML; + const submitButton = document.getElementById('storeOmbiDefaults') as HTMLButtonElement; + submitButton.disabled = false; + submitButton.textContent = 'Submit'; + addAttr(submitButton, "btn-primary"); + rmAttr(submitButton, "btn-success"); + rmAttr(submitButton, "btn-danger"); + ombiDefaultsModal.show(); + } + } + }); +}; + +(document.getElementById('storeOmbiDefaults') as HTMLButtonElement).onclick = function (): void { + let button = this as HTMLButtonElement; + button.disabled = true; + button.innerHTML = + '' + + 'Loading...'; + const radio = document.querySelector('input[name=ombiRadios]:checked') as HTMLInputElement; + const data = { + "id": radio.id.replace("ombiDefault_", "") + }; + _post("/setOmbiDefaults", data, function (): void { + if (this.readyState == 4) { + if (this.status == 200 || this.status == 204) { + button.textContent = "Success"; + addAttr(button, "btn-success"); + rmAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + button.disabled = false; + setTimeout((): void => ombiDefaultsModal.hide(), 1000); + } else { + button.textContent = "Failed"; + rmAttr(button, "btn-primary"); + addAttr(button, "btn-danger"); + setTimeout((): void => { + button.textContent = "Submit"; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-danger"); + button.disabled = false; + }, 1000); + } + } + }); +}; + + + + diff --git a/data/static/ts/serialize.ts b/data/static/ts/serialize.ts new file mode 100644 index 0000000..4f8c165 --- /dev/null +++ b/data/static/ts/serialize.ts @@ -0,0 +1,35 @@ +function serializeForm(id: string): Object { + const form = document.getElementById(id) as HTMLFormElement; + let formData = {}; + for (let i = 0; i < form.elements.length; i++) { + const el = form.elements[i]; + if ((el as HTMLInputElement).type == "submit") { + continue; + } + let name = (el as HTMLInputElement).name; + if (!name) { + name = el.id; + } + switch ((el as HTMLInputElement).type) { + case "checkbox": + formData[name] = (el as HTMLInputElement).checked; + break; + case "text": + case "password": + case "email": + case "number": + formData[name] = (el as HTMLInputElement).value; + break; + case "select-one": + case "select": + let val: string = (el as HTMLSelectElement).value.toString(); + if (!isNaN(val as any)) { + formData[name] = +val; + } else { + formData[name] = val; + } + break; + } + } + return formData; +} diff --git a/data/static/ts/settings.ts b/data/static/ts/settings.ts new file mode 100644 index 0000000..3e8266e --- /dev/null +++ b/data/static/ts/settings.ts @@ -0,0 +1,344 @@ +var config: Object = {}; +var modifiedConfig: Object = {}; + +function sendConfig(restart?: boolean): void { + modifiedConfig["restart-program"] = restart; + _post("/modifyConfig", modifiedConfig, function (): void { + if (this.readyState == 4) { + const save = document.getElementById("settingsSave") as HTMLButtonElement + if (this.status == 200 || this.status == 204) { + save.textContent = "Success"; + addAttr(save, "btn-success"); + rmAttr(save, "btn-primary"); + setTimeout((): void => { + save.textContent = "Save"; + addAttr(save, "btn-primary"); + rmAttr(save, "btn-success"); + }, 1000); + } else { + save.textContent = "Save"; + } + if (restart) { + refreshModal.show(); + } + } + }); +} + +(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => { + aboutModal.show(); +}; + +const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/getConfig", null, function (): void { + if (this.readyState == 4 && this.status == 200) { + settingsList.textContent = ''; + config = this.response; + for (const i in config["order"]) { + const section: string = config["order"][i] + const sectionCollapse = document.createElement('div') as HTMLDivElement; + Unfocus(sectionCollapse); + sectionCollapse.id = section; + + const title: string = config[section]["meta"]["name"]; + const description: string = config[section]["meta"]["description"]; + const entryListID: string = `${section}_entryList`; + // const footerID: string = `${section}_footer`; + + sectionCollapse.innerHTML = ` +
    + ${description} +
    +
    +
    + `; + + for (const x in config[section]["order"]) { + const entry: string = config[section]["order"][x]; + if (entry == "meta") { + continue; + } + let entryName: string = config[section][entry]["name"]; + let required = false; + if (config[section][entry]["required"]) { + entryName += ` *`; + required = true; + } + if (config[section][entry]["requires_restart"]) { + entryName += ` R`; + } + if ("description" in config[section][entry]) { + entryName +=` + + `; + } + const entryValue: boolean | string = config[section][entry]["value"]; + const entryType: string = config[section][entry]["type"]; + const entryGroup = document.createElement('div'); + if (entryType == "bool") { + entryGroup.classList.add("form-check"); + entryGroup.innerHTML = ` + + + `; + (entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void { + const me = this as HTMLInputElement; + for (const y in config["order"]) { + const sect: string = config["order"][y]; + for (const z in config[sect]["order"]) { + const ent: string = config[sect]["order"][z]; + if (`${sect}_${config[sect][ent]['depends_true']}` == me.id) { + (document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked); + } else if (`${sect}_${config[sect][ent]['depends_false']}` == me.id) { + (document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked; + } + } + } + }; + } else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) { + entryGroup.classList.add("form-group"); + entryGroup.innerHTML = ` + + + `; + } else if (entryType == 'select') { + entryGroup.classList.add("form-group"); + const entryOptions: Array = config[section][entry]["options"]; + let innerGroup = ` + + `; + entryGroup.innerHTML = innerGroup; + } + sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); + } + + settingsList.innerHTML += ` + + `; + settingsContent.appendChild(sectionCollapse); + } + if (callback) { + callback(); + } + } +}); + +interface Profile { + Admin: boolean; + LibraryAccess: string; + FromUser: string; +} + +(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles); + +const populateProfiles = (noTable?: boolean): void => _get("/getProfiles", null, function (): void { + if (this.readyState == 4 && this.status == 200) { + const profileList = document.getElementById('profileList'); + profileList.textContent = ''; + availableProfiles = [this.response["default_profile"]]; + for (let name in this.response) { + if (name != availableProfiles[0]) { + availableProfiles.push(name); + } + if (!noTable && name != "default_profile") { + const profile: Profile = { + Admin: this.response[name]["admin"], + LibraryAccess: this.response[name]["libraries"], + FromUser: this.response[name]["fromUser"] + }; + profileList.innerHTML += ` + ${name} + + ${profile.FromUser} + ${profile.Admin ? "Yes" : "No"} + ${profile.LibraryAccess} + + `; + } + } + } +}); + +const setDefaultProfile = (name: string): void => _post("/setDefaultProfile", { "name": name }, function (): void { + if (this.readyState == 4) { + if (this.status != 200) { + (document.getElementById(`defaultProfile_${availableProfiles[0]}`) as HTMLInputElement).checked = true; + (document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false; + } else { + generateInvites(); + } + } +}); + +const deleteProfile = (name: string): void => _post("/deleteProfile", { "name": name }, function (): void { + if (this.readyState == 4 && this.status == 200) { + populateProfiles(); + } +}); + +const createProfile = (): void => _get("/getUsers", null, function (): void { + if (this.readyState == 4 && this.status == 200) { + jfUsers = this.response["users"]; + populateRadios(); + const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement; + submitButton.disabled = false; + submitButton.textContent = 'Create'; + addAttr(submitButton, "btn-primary"); + rmAttr(submitButton, "btn-danger"); + rmAttr(submitButton, "btn-success"); + document.getElementById('defaultsTitle').textContent = `Create Profile`; + document.getElementById('userDefaultsDescription').textContent = ` + Create an account and configure it to your liking, then choose it from below to store the settings as a profile. Profiles can be specified per invite, so that any new user on that invite will have the settings applied.`; + document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`; + (document.getElementById('defaultsSource') as HTMLSelectElement).value = 'fromUser'; + document.getElementById('defaultsSourceSection').classList.add('unfocused'); + (document.getElementById('storeDefaults') as HTMLButtonElement).onclick = storeProfile; + Focus(document.getElementById('newProfileBox')); + (document.getElementById('newProfileName') as HTMLInputElement).value = ''; + Focus(document.getElementById('defaultUserRadios')); + userDefaultsModal.show(); + } +}); + +function storeProfile(): void { + this.disabled = true; + this.innerHTML = + '' + + 'Loading...'; + const button = document.getElementById('storeDefaults') as HTMLButtonElement; + const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement + const name = (document.getElementById('newProfileName') as HTMLInputElement).value; + let id = radio.id.replace("default_", ""); + let data = { + "name": name, + "id": id, + "homescreen": false + } + if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) { + data["homescreen"] = true; + } + _post("/createProfile", data, function (): void { + if (this.readyState == 4) { + if (this.status == 200 || this.status == 204) { + button.textContent = "Success"; + addAttr(button, "btn-success"); + rmAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + button.disabled = false; + setTimeout((): void => { + button.textContent = "Create"; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-success"); + button.disabled = false; + userDefaultsModal.hide(); + + }, 1000); + populateProfiles(); + generateInvites(); + } else { + if ("error" in this.response) { + button.textContent = this.response["error"]; + } else if (("policy" in this.response) || ("homescreen" in this.response)) { + button.textContent = "Failed (check console)"; + } else { + button.textContent = "Failed"; + } + addAttr(button, "btn-danger"); + rmAttr(button, "btn-primary"); + setTimeout((): void => { + button.textContent = "Create"; + addAttr(button, "btn-primary"); + rmAttr(button, "btn-danger"); + button.disabled = false; + }, 1000); + } + } + }); +} + +function showSetting(id: string, runBefore?: () => void): void { + const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf; + for (let i = 0; i < els.length; i++) { + const el = els[i]; + if (el.id != `${id}_button`) { + rmAttr(el, "active"); + } + const sectEl = document.getElementById(el.id.replace("_button", "")); + if (sectEl.id != id) { + Unfocus(sectEl); + } + } + addAttr(document.getElementById(`${id}_button`), "active"); + const section = document.getElementById(id); + if (runBefore) { + runBefore(); + } + Focus(section); + if (screen.width <= 1100) { + // ugly + setTimeout((): void => section.scrollIntoView({ block: "center", behavior: "smooth" }), 200); + } +} + +// (document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => openSettings(document.getElementById('settingsList'), document.getElementById('settingsList'), (): void => settingsModal.show()); + +(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void { + modifiedConfig = {}; + const save = this as HTMLButtonElement; + let restartSettingsChanged = false; + let settingsChanged = false; + for (const i in config["order"]) { + const section = config["order"][i]; + for (const x in config[section]["order"]) { + const entry = config[section]["order"][x]; + if (entry == "meta") { + continue; + } + let val: string; + const entryID = `${section}_${entry}`; + const el = document.getElementById(entryID) as HTMLInputElement; + if (el.type == "checkbox") { + val = el.checked.toString(); + } else { + val = el.value.toString(); + } + if (val != config[section][entry]["value"].toString()) { + if (!(section in modifiedConfig)) { + modifiedConfig[section] = {}; + } + modifiedConfig[section][entry] = val; + settingsChanged = true; + if (config[section][entry]["requires_restart"]) { + restartSettingsChanged = true; + } + } + } + } + const spinnerHTML = ` + + Loading...`; + if (restartSettingsChanged) { + save.innerHTML = spinnerHTML; + (document.getElementById('applyRestarts') as HTMLButtonElement).onclick = (): void => sendConfig(); + const restartButton = document.getElementById('applyAndRestart') as HTMLButtonElement; + if (restartButton) { + restartButton.onclick = (): void => sendConfig(true); + } + restartModal.show(); + } else if (settingsChanged) { + save.innerHTML = spinnerHTML; + sendConfig(); + } +}; + +(document.getElementById('restartModalCancel') as HTMLButtonElement).onclick = (): void => { + document.getElementById('settingsSave').textContent = "Save"; +}; diff --git a/data/static/ts/tsconfig.json b/data/static/ts/tsconfig.json new file mode 100644 index 0000000..b380a8b --- /dev/null +++ b/data/static/ts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outDir": "../data/static", + "target": "es6", + "lib": ["dom", "es2017"], + "types": ["jquery"] + } +} diff --git a/views.go b/views.go index c91d527..305ca34 100644 --- a/views.go +++ b/views.go @@ -33,7 +33,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { gc.HTML(http.StatusOK, "form.html", gin.H{ "bs5": app.config.Section("ui").Key("bs5").MustBool(false), "cssFile": app.cssFile, - "contactMessage": app.config.Section("ui").Key("contac_message").String(), + "contactMessage": app.config.Section("ui").Key("contact_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(), "successMessage": app.config.Section("ui").Key("success_message").String(), "jfLink": app.config.Section("jellyfin").Key("public_server").String(), @@ -46,7 +46,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { gc.HTML(404, "invalidCode.html", gin.H{ "bs5": app.config.Section("ui").Key("bs5").MustBool(false), "cssFile": app.cssFile, - "contactMessage": app.config.Section("ui").Key("contac_message").String(), + "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) } }