From 32b8ed4aa23b34abc99a20e920ab0c8eaa54a2ee Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 21 Sep 2020 22:03:20 +0100 Subject: [PATCH] rewrite* most web ui code in typescript i wanted to split up the web ui components into multiple files, and figured it'd be a good chance to try out typescript. run make typescript to compile everything in ts/ and put it in data/static/. This is less of a rewrite and more of a refactoring, most of it still works the same but bits have been cleaned up too. Remaining javascript found in setup.js and form.html --- .gitignore | 2 + Makefile | 34 +- api.go | 1 + data/static/accounts.js | 356 ----------- data/static/admin.js | 1184 ------------------------------------- data/static/serialize.js | 32 - data/templates/admin.html | 90 +-- package-lock.json | 18 + package.json | 1 + ts/accounts.ts | 126 ++-- ts/admin.ts | 271 +++++++++ ts/animation.ts | 79 +++ ts/bs4.ts | 38 ++ ts/bs5.ts | 36 ++ ts/invites.ts | 362 ++++++++++++ ts/ombi.ts | 83 +++ ts/serialize.ts | 34 ++ ts/settings.ts | 206 +++++++ ts/tsconfig.json | 8 + tsconfig.json | 6 - 20 files changed, 1235 insertions(+), 1732 deletions(-) delete mode 100644 data/static/accounts.js delete mode 100644 data/static/admin.js delete mode 100644 data/static/serialize.js create mode 100644 ts/admin.ts create mode 100644 ts/animation.ts create mode 100644 ts/bs4.ts create mode 100644 ts/bs5.ts create mode 100644 ts/invites.ts create mode 100644 ts/ombi.ts create mode 100644 ts/serialize.ts create mode 100644 ts/settings.ts create mode 100644 ts/tsconfig.json delete mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index cfd2188..6dafb29 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ scss/*.css* scss/bs4/*.css* scss/bs5/*.css* data/static/*.css +data/static/*.js +!data/static/setup.js data/config-base.json data/config-default.ini data/*.html diff --git a/Makefile b/Makefile index a8b8b40..90ba951 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,44 @@ configuration: - echo "Fixing config-base" + $(info Fixing config-base) python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json - echo "Generating config-default.ini" + $(info Generating config-default.ini) python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version git sass: - echo "Getting libsass" + $(info Getting libsass) python3 -m pip install libsass - echo "Getting node dependencies" + $(info Getting node dependencies) python3 scss/get_node_deps.py - echo "Compiling sass" + $(info Compiling sass) python3 scss/compile.py sass-headless: - echo "Getting libsass" + $(info Getting libsass) python3 -m pip install libsass - echo "Getting node dependencies" + $(info Getting node dependencies) python3 scss/get_node_deps.py - echo "Compiling sass" + $(info Compiling sass) python3 scss/compile.py -y mail-headless: - echo "Generating email html" + $(info Generating email html) python3 mail/generate.py -y mail: - echo "Generating email html" + $(info Generating email html) python3 mail/generate.py +typescript: + $(info Compiling typescript) + -npx tsc -p ts/ + version: python3 version.py auto version.go compile: - echo "Downloading deps" + $(info Downloading deps) go mod download - echo "Building" + $(info Building) mkdir -p build CGO_ENABLED=0 go build -o build/jfa-go *.go @@ -42,14 +46,14 @@ compress: upx --lzma build/jfa-go copy: - echo "Copying data" + $(info Copying data) cp -r data build/ install: cp -r build $(DESTDIR)/jfa-go -all: configuration sass mail version compile copy -headless: configuration sass-headless mail-headless version compile copy +all: configuration sass mail version typescript compile copy +headless: configuration sass-headless mail-headless version typescript compile copy diff --git a/api.go b/api.go index e5c60a5..063d1f7 100644 --- a/api.go +++ b/api.go @@ -230,6 +230,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { } } } + app.jf.cacheExpiry = time.Now() } func (app *appContext) NewUser(gc *gin.Context) { diff --git a/data/static/accounts.js b/data/static/accounts.js deleted file mode 100644 index b651a5f..0000000 --- a/data/static/accounts.js +++ /dev/null @@ -1,356 +0,0 @@ -document.getElementById('selectAll').onclick = function() { - const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); - for (check of checkboxes) { - check.checked = this.checked; - } - checkCheckboxes(); -}; - -function checkCheckboxes() { - const defaultsButton = document.getElementById('accountsTabSetDefaults'); - const deleteButton = document.getElementById('accountsTabDelete'); - const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); - let checked = 0; - for (check of checkboxes) { - if (check.checked) { - checked++; - } - } - if (checked == 0) { - defaultsButton.classList.add('unfocused'); - deleteButton.classList.add('unfocused'); - } else { - if (defaultsButton.classList.contains('unfocused')) { - defaultsButton.classList.remove('unfocused'); - } - if (deleteButton.classList.contains('unfocused')) { - deleteButton.classList.remove('unfocused'); - } - if (checked == 1) { - deleteButton.textContent = 'Delete User'; - } else { - deleteButton.textContent = 'Delete Users'; - } - } -} - -document.getElementById('deleteModalNotify').onclick = function() { - const textbox = document.getElementById('deleteModalReasonBox'); - if (this.checked && textbox.classList.contains('unfocused')) { - textbox.classList.remove('unfocused'); - } else if (!this.checked) { - textbox.classList.add('unfocused'); - } -}; - -document.getElementById('accountsTabDelete').onclick = function() { - const deleteButton = this; - let selected = []; - const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); - for (check of checkboxes) { - if (check.checked) { - selected.push(check.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; - document.getElementById('deleteModalNotify').checked = false; - document.getElementById('deleteModalNotifyLabel').textContent = msg; - document.getElementById('deleteModalReason').value = ''; - document.getElementById('deleteModalReasonBox').classList.add('unfocused'); - document.getElementById('deleteModalSend').textContent = 'Delete'; - - document.getElementById('deleteModalSend').onclick = function() { - const button = this; - const send = { - 'users': selected, - 'notify': document.getElementById('deleteModalNotify').checked, - 'reason': document.getElementById('deleteModalReason').value - }; - let req = new XMLHttpRequest(); - req.open("POST", "/deleteUser", true); - req.responseType = 'json'; - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 500) { - if ("error" in req.response) { - button.textContent = 'Failed'; - } else { - button.textContent = 'Partial fail (check console)'; - console.log(req.response); - } - setTimeout(function() { - deleteModal.hide(); - deleteButton.classList.add('unfocused'); - }, 4000); - } else { - deleteButton.classList.add('unfocused'); - deleteModal.hide(); - } - populateUsers(); - checkCheckboxes(); - } - }; - req.send(JSON.stringify(send)); - }; - deleteModal.show(); -} - -var jfUsers = []; - -function validEmail(email) { - const re = /\S+@\S+\.\S+/; - return re.test(email); -} - -function changeEmail(icon, id) { - const iconContent = icon.outerHTML; - icon.setAttribute("class", ""); - const entry = icon.nextElementSibling; - const ogEmail = entry.value; - entry.readOnly = false; - entry.classList.remove('form-control-plaintext'); - entry.classList.add('form-control'); - if (entry.value == "") { - entry.placeholder = 'Address'; - } - const tick = document.createElement('i'); - tick.classList.add("fa", "fa-check", "d-inline-block", "icon-button", "text-success"); - tick.setAttribute('style', 'margin-left: 0.5rem; margin-right: 0.5rem;'); - tick.onclick = function() { - const newEmail = entry.value; - if (!validEmail(newEmail) || newEmail == ogEmail) { - return - } - cross.remove(); - this.outerHTML = ` -
- Saving... -
`; - //this.remove(); - let send = {}; - send[id] = newEmail; - let req = new XMLHttpRequest(); - req.open("POST", "/modifyEmails", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - entry.nextElementSibling.remove(); - } else { - entry.value = ogEmail; - } - } - }; - req.send(JSON.stringify(send)); - icon.outerHTML = iconContent; - entry.readOnly = true; - entry.classList.remove('form-control'); - entry.classList.add('form-control-plaintext'); - entry.placeholder = ''; - }; - const cross = document.createElement('i'); - cross.classList.add("fa", "fa-close", "d-inline-block", "icon-button", "text-danger"); - cross.onclick = function() { - tick.remove(); - this.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); -} - -function populateUsers() { - const acList = document.getElementById('accountsList'); - acList.innerHTML = ` -
- Getting Users... - -
`; - acList.parentNode.querySelector('thead').classList.add('unfocused'); - const accountsList = document.createElement('tbody'); - accountsList.id = 'accountsList'; - const generateEmail = function(id, name, email) { - let entry = document.createElement('div'); - // entry.classList.add('py-1'); - entry.id = 'email_' + id; - let emailValue = email; - if (email === undefined) { - emailValue = ""; - } - entry.innerHTML = ` - - - `; - return entry.outerHTML - }; - const template = function(id, username, email, lastActive, admin) { - let isAdmin = "No"; - if (admin) { - isAdmin = "Yes"; - } - let fci = "form-check-input"; - if (bsVersion != 5) { - fci = ""; - } - return ` - - ${username} - ${generateEmail(id, name, email)} - ${lastActive} - ${isAdmin} - `; - }; - - let req = new XMLHttpRequest(); - req.responseType = 'json'; - req.open("GET", "/getUsers", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - jfUsers = req.response['users']; - for (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); - } - const header = acList.parentNode.querySelector('thead'); - if (header.classList.contains('unfocused')) { - header.classList.remove('unfocused'); - } - acList.replaceWith(accountsList); - } - } - }; - req.send(); -} - -document.getElementById('selectAll').checked = false; - -document.getElementById('accountsTabSetDefaults').onclick = function() { - const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); - let userIDs = []; - for (check of checkboxes) { - if (check.checked) { - userIDs.push(check.id.replace('select_', '')); - } - } - if (userIDs.length == 0) { - return; - } - populateRadios(); - let userstring = 'user'; - if (userIDs.length > 1) { - userstring += 's'; - } - document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`; - document.getElementById('userDefaultsDescription').textContent = ` - Create an account and configure it to your liking, then choose it from below to apply to your selected users.`; - document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`; - if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) { - document.getElementById('defaultsSourceSection').classList.remove('unfocused'); - } - document.getElementById('defaultsSource').value = 'userTemplate'; - document.getElementById('defaultUserRadios').classList.add('unfocused'); - document.getElementById('storeDefaults').onclick = function() { - storeDefaults(userIDs); - }; - userDefaultsModal.show(); -}; - -document.getElementById('defaultsSource').addEventListener('change', function() { - const radios = document.getElementById('defaultUserRadios'); - if (this.value == 'userTemplate') { - radios.classList.add('unfocused'); - } else if (radios.classList.contains('unfocused')) { - radios.classList.remove('unfocused'); - } -}) - -document.getElementById('newUserCreate').onclick = function() { - const ogText = this.textContent; - this.innerHTML = ` - Creating...`; - const email = document.getElementById('newUserEmail').value; - var username = email; - if (document.getElementById('newUserName') != null) { - username = document.getElementById('newUserName').value; - } - const password = document.getElementById('newUserPassword').value; - if (!validEmail(email) && email != "") { - return; - } - const send = { - 'username': username, - 'password': password, - 'email': email - } - let req = new XMLHttpRequest() - req.open("POST", "/newUserAdmin", true); - req.responseType = 'json'; - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - const button = this; - req.onreadystatechange = function() { - if (this.readyState == 4) { - button.textContent = ogText; - if (this.status == 200) { - if (button.classList.contains('btn-primary')) { - button.classList.remove('btn-primary'); - } - button.classList.add('btn-success'); - button.textContent = 'Success'; - setTimeout(function() { - if (button.classList.contains('btn-success')) { - button.classList.remove('btn-success'); - } - button.classList.add('btn-primary'); - button.textContent = ogText; - newUserModal.hide(); - }, 1000); - } else { - if (button.classList.contains('btn-primary')) { - button.classList.remove('btn-primary'); - } - button.classList.add('btn-danger'); - if ("error" in req.response) { - button.textContent = req.response["error"]; - } else { - button.textContent = 'Failed'; - } - setTimeout(function() { - if (button.classList.contains('btn-danger')) { - button.classList.remove('btn-danger'); - } - button.classList.add('btn-primary'); - button.textContent = ogText; - }, 2000); - } - } - }; - req.send(JSON.stringify(send)); -} - -document.getElementById('accountsTabAddUser').onclick = function() { - document.getElementById('newUserEmail').value = ''; - document.getElementById('newUserPassword').value = ''; - if (document.getElementById('newUserName') != null) { - document.getElementById('newUserName').value = ''; - } - newUserModal.show(); -}; diff --git a/data/static/admin.js b/data/static/admin.js deleted file mode 100644 index 257c11b..0000000 --- a/data/static/admin.js +++ /dev/null @@ -1,1184 +0,0 @@ -const tabs = { - invitesEl: document.getElementById('invitesTab'), - accountsEl: document.getElementById('accountsTab'), - invitesTabButton: document.getElementById('invitesTabButton'), - accountsTabButton: document.getElementById('accountsTabButton'), - invites: function() { - if (tabs.invitesEl.classList.contains('unfocused')) { - tabs.accountsEl.classList.add('unfocused'); - tabs.invitesEl.classList.remove('unfocused'); - } - if (tabs.accountsTabButton.classList.contains("active")) { - tabs.accountsTabButton.classList.remove("active"); - } - tabs.invitesTabButton.classList.add("active"); - }, - accounts: function() { - populateUsers(); - if (tabs.accountsEl.classList.contains('unfocused')) { - tabs.invitesEl.classList.add('unfocused'); - tabs.accountsEl.classList.remove('unfocused'); - } - if (tabs.invitesTabButton.classList.contains("active")) { - tabs.invitesTabButton.classList.remove("active"); - tabs.accountsTabButton.classList.add("active"); - } - } -}; - -tabs.invitesTabButton.onclick = tabs.invites; -tabs.accountsTabButton.onclick = tabs.accounts; - -tabs.invites(); - -// Used for theme change animation -function whichTransitionEvent() { - let t; - let el = document.createElement('fakeelement'); - let transitions = { - 'transition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'MozTransition': 'transitionend', - 'WebkitTransition': 'webkitTransitionEnd' - }; - - for (t in transitions) { - if (el.style[t] !== undefined) { - return transitions[t]; - } - } -} -var transitionEndEvent = whichTransitionEvent(); - -// Toggles between light and dark themes -function toggleCSS() { - let Els = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]'); - let cssEl = Els[0] - let remove = false; - if (Els.length != 1) { - cssEl = Els[1] - remove = true - } - let href = "bs" + bsVersion; - if (cssEl.href.includes(href + "-jf")) { - href += ".css"; - } else { - href += "-jf.css"; - } - let newEl = cssEl.cloneNode(true); - newEl.href = href - cssEl.parentNode.insertBefore(newEl, cssEl.nextSibling); - if (remove) { - Els[0].remove() - } - document.cookie = "css=" + href; -} - -// Toggles between light and dark themes, but runs animation if necessary (dependent on window width for performance) -var buttonWidth = 0; -function toggleCSSAnim(el) { - let switchToColor = window.getComputedStyle(document.body, null).backgroundColor; - let maxWidth = 1500; - if (window.innerWidth < maxWidth) { - // Calculate minimum radius to cover whole screen - let radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)); - let currentRadius = el.getBoundingClientRect().width / 2; - let 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() { - if (this.style.transform.length != 0) { - toggleCSS(); - this.style.removeProperty('transform'); - document.body.classList.add('smooth-transition'); - }; - }, false); - } else { - toggleCSS(); - el.style.color = switchToColor; - } -} - -var buttonColor = "custom"; -// Predefined colors for 'theme' button -if (cssFile.includes("jf")) { - buttonColor = "rgb(255,255,255)"; -} else if (cssFile == ('bs' + bsVersion + '.css')) { - buttonColor = "rgb(16,16,16)"; -} - -if (buttonColor != "custom") { - let fakeButton = document.createElement('i'); - fakeButton.classList.add('fa', 'fa-circle', 'circle'); - fakeButton.style = `color: ${buttonColor}; margin-left: 0.4rem;`; - fakeButton.id = "fakeButton"; - let switchButton = document.createElement('button'); - switchButton.classList.add('btn', 'btn-secondary'); - switchButton.textContent = "Theme"; - switchButton.onclick = function() { - let fakeButton = document.getElementById('fakeButton'); - toggleCSSAnim(fakeButton); - }; - let group = document.getElementById('headerButtons'); - switchButton.appendChild(fakeButton); - group.appendChild(switchButton); -} - - -var loginModal = createModal('login'); -var settingsModal = createModal('settingsMenu'); -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 = []; -// god this is an ugly way to do it -// Parsed invite: [, , <1: Empty invite (no delete/link), 0: Actual invite>, , , [], , , , ] -function parseInvite(invite, empty = false) { - if (empty) { - return ["None", "", 1]; - } - let i = [invite["code"], "", 0, invite["email"]]; - let time = "" - for (m of ["days", "hours", "minutes"]) { - if (invite[m] != 0) { - time += `${invite[m]}${m[0]} `; - } - } - i[1] = `Expires in ${time.slice(0, -1)}`; - if ('remaining-uses' in invite) { - i[4] = invite['remaining-uses']; - } - if (invite['no-limit']) { - i[4] = '∞'; - } - if ('used-by' in invite) { - i[5] = invite['used-by']; - } else { - i[5] = []; - } - if ('created' in invite) { - i[6] = invite['created']; - } - if ('notify-expiry' in invite) { - i[7] = invite['notify-expiry']; - } - if ('notify-creation' in invite) { - i[8] = invite['notify-creation']; - } - if ('profile' in invite) { - i[9] = invite['profile']; - } - return i; -} - -function addItem(parsedInvite) { - let links = document.getElementById('invites'); - let itemContainer = document.createElement('div'); - itemContainer.id = parsedInvite[0]; - let listItem = document.createElement('div'); - // listItem.id = parsedInvite[0]; - listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block'); - - let code = document.createElement('div'); - code.classList.add('d-flex', 'align-items-center', 'font-monospace'); - code.setAttribute('style', 'width: 40%;'); - let codeLink = document.createElement('a'); - codeLink.textContent = parsedInvite[0].replace(/-/g, '-'); - - code.appendChild(codeLink); - - listItem.appendChild(code); - - let listRight = document.createElement('div'); - listRight.setAttribute('style', 'text-align: right;'); - let listText = document.createElement('span'); - listText.id = parsedInvite[0] + '_expiry'; - listText.setAttribute('style', 'margin-right: 1rem;'); - listText.textContent = parsedInvite[1]; - - listRight.appendChild(listText); - - if (parsedInvite[2] == 0) { - let inviteCode = window.location.href.split('#')[0] + 'invite/' + parsedInvite[0]; - // - codeLink.href = inviteCode; - codeLink.classList.add('invite-link'); - let copyButton = document.createElement('i'); - copyButton.onclick = function() { toClipboard(inviteCode); }; - copyButton.classList.add('fa', 'fa-clipboard', 'icon-button'); - copyButton.setAttribute('style', 'margin-right: 0.5rem; margin-left: 0.5rem;'); - - code.appendChild(copyButton); - - if (parsedInvite[3] !== undefined) { - let sentTo = document.createElement('span'); - sentTo.classList.add('text-muted'); - sentTo.setAttribute('style', 'margin-left: 0.4rem; font-style: italic, font-size: 0.75rem;'); - if (!parsedInvite[3].includes('Failed to send to')) { - sentTo.textContent = "Sent to "; - } - sentTo.textContent += parsedInvite[3]; - - code.appendChild(sentTo); - } - - let deleteButton = document.createElement('button'); - deleteButton.onclick = function() { deleteInvite(parsedInvite[0]); }; - deleteButton.classList.add('btn', 'btn-outline-danger'); - deleteButton.textContent = "Delete"; - - let block = document.createElement('div'); - block.setAttribute('style', 'display: inline-block;'); - block.appendChild(deleteButton); - let dropButton = document.createElement('i'); - dropButton.classList.add('fa', 'fa-angle-down', 'collapsed', 'icon-button', 'not-rotated'); - dropButton.setAttribute('style', 'padding: 1rem; margin: -1rem -1rem -1rem 0;'); - dropButton.setAttribute('data-toggle', 'collapse'); - dropButton.setAttribute('aria-expanded', 'false'); - dropButton.setAttribute('data-target', '#' + CSS.escape(parsedInvite[0]) + '_collapse'); - dropButton.onclick = function() { - if (this.classList.contains('rotated')) { - this.classList.remove('rotated'); - this.classList.add('not-rotated'); - } else { - this.classList.remove('not-rotated'); - this.classList.add('rotated'); - } - }; - dropButton.setAttribute('style', 'margin-left: 1rem;'); - block.appendChild(dropButton); - listRight.appendChild(block); - } - - listItem.appendChild(listRight); - itemContainer.appendChild(listItem); - if (parsedInvite[2] == 0) { - let itemDropdown = document.createElement('div'); - itemDropdown.id = parsedInvite[0] + '_collapse'; - itemDropdown.classList.add('collapse'); - - let dropdownContent = document.createElement('div'); - dropdownContent.classList.add('container', 'row', 'align-items-start', 'card-body'); - - let dropdownLeft = document.createElement('div'); - dropdownLeft.classList.add('col'); - - let leftList = document.createElement('ul'); - leftList.classList.add('list-group', 'list-group-flush'); - - let profileBox = document.createElement('li'); - profileBox.classList.add('input-group', 'py-1'); - let prof = ` - - `; - profileBox.innerHTML = prof; - leftList.appendChild(profileBox); - // 9 is profileName! availableProfiles - - if (typeof(parsedInvite[6]) != 'undefined') { - let createdDate = document.createElement('li'); - createdDate.classList.add('list-group-item', 'py-1'); - createdDate.textContent = `Created: ${parsedInvite[6]}`; - leftList.appendChild(createdDate); - } - - let remainingUses = document.createElement('li'); - remainingUses.classList.add('list-group-item', 'py-1'); - remainingUses.id = parsedInvite[0] + '_remainingUses'; - remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`; - leftList.appendChild(remainingUses); - - dropdownLeft.appendChild(leftList); - dropdownContent.appendChild(dropdownLeft); - - if (notifications_enabled) { - let dropdownMiddle = document.createElement('div'); - dropdownMiddle.id = parsedInvite[0] + '_notifyButtons'; - dropdownMiddle.classList.add('col'); - - let middleList = document.createElement('ul'); - middleList.classList.add('list-group', 'list-group-flush'); - middleList.textContent = 'Notify on:'; - - let notifyExpiry = document.createElement('li'); - notifyExpiry.classList.add('list-group-item', 'py-1', 'form-check'); - notifyExpiry.innerHTML = ` - - - `; - if (typeof(parsedInvite[7]) == 'boolean') { - notifyExpiry.getElementsByTagName('input')[0].checked = parsedInvite[7]; - } - - notifyExpiry.getElementsByTagName('input')[0].onclick = function() { - let req = new XMLHttpRequest(); - var thisEl = this; - let send = {}; - let code = thisEl.id.replace('_notifyExpiry', ''); - send[code] = {}; - send[code]['notify-expiry'] = thisEl.checked; - req.open("POST", "/setNotify", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status != 200) { - thisEl.checked = !thisEl.checked; - } - }; - req.send(JSON.stringify(send)); - }; - middleList.appendChild(notifyExpiry); - - let notifyCreation = document.createElement('li'); - notifyCreation.classList.add('list-group-item', 'py-1', 'form-check'); - notifyCreation.innerHTML = ` - - - `; - if (typeof(parsedInvite[8]) == 'boolean') { - notifyCreation.getElementsByTagName('input')[0].checked = parsedInvite[8]; - } - notifyCreation.getElementsByTagName('input')[0].onclick = function() { - let req = new XMLHttpRequest(); - var thisEl = this; - let send = {}; - let code = thisEl.id.replace('_notifyCreation', ''); - send[code] = {}; - send[code]['notify-creation'] = thisEl.checked; - req.open("POST", "/setNotify", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status != 200) { - thisEl.checked = !thisEl.checked; - } - }; - req.send(JSON.stringify(send)); - }; - middleList.appendChild(notifyCreation); - - dropdownMiddle.appendChild(middleList); - dropdownContent.appendChild(dropdownMiddle); - } - - - let dropdownRight = document.createElement('div'); - dropdownRight.id = parsedInvite[0] + '_usersCreated'; - dropdownRight.classList.add('col'); - if (parsedInvite[5].length != 0) { - let userList = document.createElement('ul'); - userList.classList.add('list-group', 'list-group-flush'); - userList.innerHTML = '
  • Users created:
  • '; - for (let user of parsedInvite[5]) { - let li = document.createElement('li'); - li.classList.add('list-group-item', 'py-1', 'disabled'); - let username = document.createElement('div'); - username.classList.add('d-flex', 'float-left'); - username.textContent = user[0]; - li.appendChild(username); - let date = document.createElement('div'); - date.classList.add('d-flex', 'float-right'); - date.textContent = user[1]; - li.appendChild(date); - userList.appendChild(li); - } - dropdownRight.appendChild(userList); - } - dropdownContent.appendChild(dropdownRight); - - itemDropdown.appendChild(dropdownContent); - - itemContainer.appendChild(itemDropdown); - } - links.appendChild(itemContainer); -} - -function updateInvite(parsedInvite) { - let expiry = document.getElementById(parsedInvite[0] + '_expiry'); - expiry.textContent = parsedInvite[1]; - - let remainingUses = document.getElementById(parsedInvite[0] + '_remainingUses'); - if (remainingUses) { - remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`; - } - - if (parsedInvite[5].length != 0) { - let usersCreated = document.getElementById(parsedInvite[0] + '_usersCreated'); - let dropdownRight = document.createElement('div'); - dropdownRight.id = parsedInvite[0] + '_usersCreated'; - dropdownRight.classList.add('col'); - let userList = document.createElement('ul'); - userList.classList.add('list-group', 'list-group-flush'); - userList.innerHTML = '
  • Users created:
  • '; - for (let user of parsedInvite[5]) { - let li = document.createElement('li'); - li.classList.add('list-group-item', 'py-1', 'disabled'); - let username = document.createElement('div'); - username.classList.add('d-flex', 'float-left'); - username.textContent = user[0]; - li.appendChild(username); - let date = document.createElement('div'); - date.classList.add('d-flex', 'float-right'); - date.textContent = user[1]; - li.appendChild(date); - userList.appendChild(li); - } - dropdownRight.appendChild(userList); - usersCreated.replaceWith(dropdownRight); - } - - -} - -// delete from list on page -function removeInvite(code) { - let item = document.getElementById(code); - item.parentNode.removeChild(item); -} - -function generateInvites(empty = false) { - if (empty === false) { - let req = new XMLHttpRequest(); - req.open("GET", "/getInvites", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - var data = this.response; - availableProfiles = data['profiles']; - if (data['invites'] == null || data['invites'].length == 0) { - document.getElementById('invites').textContent = ''; - addItem(parseInvite([], true)); - } else { - for (let invite of data['invites']) { - let match = false; - let items = document.getElementById('invites').children; - for (let item of items) { - if (item.id == invite['code']) { - match = true; - updateInvite(parseInvite(invite)); - } - } - if (match == false) { - addItem(parseInvite(invite)); - } - } - let items = document.getElementById('invites').children; - for (let item of items) { - var exists = false; - for (let invite of data['invites']) { - if (item.id == invite['code']) { - exists = true; - } - } - if (exists == false) { - removeInvite(item.id); - } - } - } - } - }; - req.send(); - } else if (empty === true) { - document.getElementById('invites').textContent = ''; - addItem(parseInvite([], true)); - } -} - -// actually delete invite -function deleteInvite(code) { - let send = JSON.stringify({ "code": code }); - let req = new XMLHttpRequest(); - req.open("POST", "/deleteInvite", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - generateInvites(); - } - }; - req.send(send); -} - -// Add numbers to select element -function addOptions(length, selectElement) { - for (let v = 0; v <= length; v++) { - let opt = document.createElement('option'); - opt.textContent = v; - opt.value = v; - selectElement.appendChild(opt); - } -} - -function toClipboard(str) { - const el = document.createElement('textarea'); - el.value = str; - el.setAttribute('readOnly', ''); - 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 fixCheckboxes() { - let send_to_address = [document.getElementById('send_to_address'), document.getElementById('send_to_address_enabled')] - if (send_to_address[0] != null) { - send_to_address[0].disabled = !send_to_address[1].checked; - } - let multiUseEnabled = document.getElementById('multiUseEnabled'); - let multiUseCount = document.getElementById('multiUseCount'); - let noUseLimit = document.getElementById('noUseLimit'); - multiUseCount.disabled = !multiUseEnabled.checked; - noUseLimit.checked = false; - noUseLimit.disabled = !multiUseEnabled.checked; -} - -fixCheckboxes(); - -document.getElementById('inviteForm').onsubmit = function() { - let button = document.getElementById('generateSubmit'); - button.disabled = true; - button.innerHTML = - '' + - 'Loading...'; - send_object = serializeForm('inviteForm'); - if (!send_object['multiple-uses'] || send_object['no-limit']) { - delete send_object['remaining-uses']; - } - if (document.getElementById('send_to_address') != null) { - if (send_object['send_to_address_enabled']) { - send_object['email'] = send_object['send_to_address']; - delete send_object['send_to_address']; - delete send_object['send_to_address_enabled']; - } - } - let send = JSON.stringify(send_object); - let req = new XMLHttpRequest(); - req.open("POST", "/generateInvite", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - button.textContent = 'Generate'; - button.disabled = false; - generateInvites(); - } - }; - req.send(send); - return false; -}; - -function tryLogin(username, password, modal, button, callback) { - let req = new XMLHttpRequest(); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status != 200) { - let errormsg = req.response["error"]; - if (errormsg == "") { - errormsg = "Unknown error" - } - if (modal) { - button.disabled = false; - button.textContent = errormsg; - if (!button.classList.contains('btn-danger')) { - button.classList.add('btn-danger'); - button.classList.remove('btn-primary'); - } - setTimeout(function () { - if (button.classList.contains('btn-danger')) { - button.classList.add('btn-primary'); - button.classList.remove('btn-danger'); - button.textContent = 'Login'; - } - }, 4000) - } else { - loginModal.show(); - } - } else { - const data = this.response; - window.token = data['token']; - generateInvites(); - const interval = setInterval(function() { generateInvites(); }, 60 * 1000); - let day = document.getElementById('days'); - addOptions(30, day); - day.selected = "0"; - let hour = document.getElementById('hours'); - addOptions(24, hour); - hour.selected = "0"; - let minutes = document.getElementById('minutes'); - addOptions(59, minutes); - minutes.selected = "30"; - checkDuration(); - if (modal) { - loginModal.hide(); - } - document.getElementById('logoutButton').setAttribute('style', ''); - } - if (typeof callback === "function") { - callback(this.status); - } - } - }; - req.open("GET", "/getToken", true); - req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password)); - req.send(); -} - -document.getElementById('loginForm').onsubmit = function() { - window.token = ""; - let details = serializeForm('loginForm'); - // let errorArea = document.getElementById('loginErrorArea'); - // errorArea.textContent = ''; - let button = document.getElementById('loginSubmit'); - if (button.classList.contains('btn-danger')) { - button.classList.add('btn-primary'); - button.classList.remove('btn-danger'); - } - button.disabled = true; - button.innerHTML = - '' + - 'Loading...'; - tryLogin(username = details['username'], password = details['password'], modal = true, button = button) - return false; -}; - -document.getElementById('openAbout').onclick = function() { - settingsModal.hide(); - aboutModal.show(); -}; - -function populateRadios() { - let radioList = document.getElementById('defaultUserRadios'); - radioList.textContent = ''; - let first = true; - for (user of jfUsers) { - let radio = document.createElement('div'); - radio.classList.add('form-check'); - let checked = 'checked'; - if (first) { - first = false; - } else { - checked = ''; - } - // radio.innerHTML = - // ``; - radio.innerHTML = ` - - `; - radioList.appendChild(radio); - } -} - -document.getElementById('openDefaultsWizard').onclick = function() { - this.disabled = true - this.innerHTML = - '' + - 'Loading...'; - let req = new XMLHttpRequest(); - req.responseType = 'json'; - req.open("GET", "/getUsers", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - jfUsers = req.response['users']; - populateRadios(); - let button = document.getElementById('openDefaultsWizard'); - button.disabled = false; - button.innerHTML = 'New User Defaults '; - let submitButton = document.getElementById('storeDefaults'); - submitButton.disabled = false; - submitButton.textContent = 'Submit'; - if (submitButton.classList.contains('btn-success')) { - submitButton.classList.remove('btn-success'); - submitButton.classList.add('btn-primary'); - } else if (submitButton.classList.contains('btn-danger')) { - submitButton.classList.remove('btn-danger'); - submitButton.classList.add('btn-primary'); - } - settingsModal.hide(); - document.getElementById('defaultsTitle').textContent = `New user defaults`; - document.getElementById('userDefaultsDescription').textContent = ` - Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.`; - document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`; - document.getElementById('defaultsSource').value = 'fromUser'; - document.getElementById('defaultsSourceSection').classList.add('unfocused'); - document.getElementById('storeDefaults').onclick = function() { - storeDefaults('all'); - }; - const list = document.getElementById('defaultUserRadios'); - if (list.classList.contains('unfocused')) { - list.classList.remove('unfocused'); - } - userDefaultsModal.show(); - } - } - }; - req.send(); -}; - -function storeDefaults(users) { - this.disabled = true; - this.innerHTML = - '' + - 'Loading...'; - let button = document.getElementById('storeDefaults'); - let radios = document.getElementsByName('defaultRadios'); - let id = ''; - for (let radio of radios) { - if (radio.checked) { - id = radio.id.replace('default_', ''); - break; - } - } - let route = '/setDefaults'; - let data = { - 'from': 'user', - 'id': id, - 'homescreen': false - }; - if (document.getElementById('defaultsSource').value == 'userTemplate') { - data['from'] = 'template'; - } - if (users != 'all') { - data['apply_to'] = users; - route = '/applySettings'; - } - if (document.getElementById('storeDefaultHomescreen').checked) { - data['homescreen'] = true; - } - let req = new XMLHttpRequest(); - req.open("POST", route, true); - req.responseType = 'json'; - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - button.textContent = "Success"; - if (button.classList.contains('btn-danger')) { - button.classList.remove('btn-danger'); - } else if (button.classList.contains('btn-primary')) { - button.classList.remove('btn-primary'); - } - button.classList.add('btn-success'); - button.disabled = false; - setTimeout(function() { - let button = document.getElementById('storeDefaults'); - button.textContent = "Submit"; - button.classList.remove('btn-success'); - button.classList.add('btn-primary'); - button.disabled = false; - userDefaultsModal.hide(); - }, 1000); - } else { - if ("error" in req.response) { - button.textContent = req.response["error"]; - } else if (("policy" in req.response) || ("homescreen" in req.response)) { - button.textContent = "Failed (Check JS Console)"; - } else { - button.textContent = "Failed"; - } - button.classList.remove('btn-primary'); - button.classList.add('btn-danger'); - setTimeout(function() { - let button = document.getElementById('storeDefaults'); - button.textContent = "Submit"; - button.classList.remove('btn-danger'); - button.classList.add('btn-primary'); - button.disabled = false; - }, 1000); - } - } - }; - req.send(JSON.stringify(data)); -}; - -var ombiDefaultsModal = ''; -if (ombiEnabled) { - ombiDefaultsModal = createModal('ombiDefaults'); - document.getElementById('openOmbiDefaults').onclick = function() { - this.disabled = true; - this.innerHTML = - '' + - 'Loading...'; - let req = new XMLHttpRequest(); - req.responseType = 'json'; - req.open("GET", "/getOmbiUsers", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - let users = req.response['users']; - let radioList = document.getElementById('ombiUserRadios'); - radioList.textContent = ''; - let first = true; - // name and id - for (user of users) { - let radio = document.createElement('div'); - radio.classList.add('radio'); - let checked = 'checked'; - if (first) { - first = false; - } else { - checked = ''; - } - radio.innerHTML = - ``; - radioList.appendChild(radio); - } - let button = document.getElementById('openOmbiDefaults'); - button.disabled = false; - button.innerHTML = 'Ombi User Defaults '; - let submitButton = document.getElementById('storeOmbiDefaults'); - submitButton.disabled = false; - submitButton.textContent = 'Submit'; - if (submitButton.classList.contains('btn-success')) { - submitButton.classList.remove('btn-success'); - submitButton.classList.add('btn-primary'); - } else if (submitButton.classList.contains('btn-danger')) { - submitButton.classList.remove('btn-danger'); - submitButton.classList.add('btn-primary'); - } - settingsModal.hide(); - ombiDefaultsModal.show(); - } - } - }; - req.send(); - }; - document.getElementById('storeOmbiDefaults').onclick = function() { - this.disabled = true; - this.innerHTML = - '' + - 'Loading...'; - let button = document.getElementById('storeOmbiDefaults'); - let radios = document.getElementsByName('ombiRadios'); - for (let radio of radios) { - if (radio.checked) { - let data = { - 'id': radio.id.slice(8), - }; - let req = new XMLHttpRequest(); - req.open("POST", "/setOmbiDefaults", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - button.textContent = "Success"; - if (button.classList.contains('btn-danger')) { - button.classList.remove('btn-danger'); - } else if (button.classList.contains('btn-primary')) { - button.classList.remove('btn-primary'); - } - button.classList.add('btn-success'); - button.disabled = false; - setTimeout(function() { ombiDefaultsModal.hide(); }, 1000); - } else { - button.textContent = "Failed"; - button.classList.remove('btn-primary'); - button.classList.add('btn-danger'); - setTimeout(function() { - let button = document.getElementById('storeOmbiDefaults'); - button.textContent = "Submit"; - button.classList.remove('btn-danger'); - button.classList.add('btn-primary'); - button.disabled = false; - }, 1000); - } - } - }; - req.send(JSON.stringify(data)); - } - } - }; -} - -generateInvites(empty = true); - -tryLogin("", "", false, callback = function(code){ - console.log(code); - if (code != 200) { - loginModal.show(); - } -}); - -document.getElementById('logoutButton').onclick = function () { - let req = new XMLHttpRequest(); - req.open("POST", "/logout", true); - req.responseType = 'json'; - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - window.token = ''; - location.reload(); - return false; - } - }; - req.send(); -} - -var config = {}; -var modifiedConfig = {}; - -document.getElementById('openSettings').onclick = function () { - let req = new XMLHttpRequest(); - req.open("GET", "/getConfig", true); - req.responseType = 'json'; - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - let settingsList = document.getElementById('settingsList'); - settingsList.textContent = ''; - config = this.response; - for (let section of config["order"]) { - let sectionCollapse = document.createElement('div'); - sectionCollapse.classList.add('collapse'); - sectionCollapse.id = section; - - let sectionTitle = config[section]['meta']['name']; - let sectionDescription = config[section]['meta']['description']; - let entryListID = section + '_entryList'; - let sectionFooter = section + '_footer'; - - let innerCollapse = ` -
    - ${sectionDescription} -
    -
    -
    - `; - - sectionCollapse.innerHTML = innerCollapse; - - for (var entry of config[section]["order"]) { - if (entry != 'meta') { - let entryName = config[section][entry]['name']; - let required = false; - if (config[section][entry]['required']) { - entryName += ' *'; - required = true; - } - if (config[section][entry]['requires_restart']) { - entryName += ' R'; - } - if (config[section][entry].hasOwnProperty('description')) { - let tooltip = ` - - `; - entryName += ' '; - entryName += tooltip; - }; - let entryValue = config[section][entry]['value']; - let entryType = config[section][entry]['type']; - let entryGroup = document.createElement('div'); - if (entryType == 'bool') { - entryGroup.classList.add('form-check'); - if (entryValue.toString() == 'true') { - var checked = true; - } else { - var checked = false; - } - entryGroup.innerHTML = ` - - - `; - entryGroup.getElementsByClassName('form-check-input')[0].required = required; - entryGroup.getElementsByClassName('form-check-input')[0].checked = checked; - entryGroup.getElementsByClassName('form-check-input')[0].onclick = function() { - var state = this.checked; - for (var sect of Object.keys(config)) { - for (var ent of Object.keys(config[sect])) { - if ((sect + '_' + config[sect][ent]['depends_true']) == this.id) { - document.getElementById(sect + '_' + ent).disabled = !state; - } else if ((sect + '_' + config[sect][ent]['depends_false']) == this.id) { - document.getElementById(sect + '_' + ent).disabled = state; - } - } - } - }; - } else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) { - entryGroup.classList.add('form-group'); - entryGroup.innerHTML = ` - - - `; - entryGroup.getElementsByClassName('form-control')[0].required = required; - } else if (entryType == 'select') { - entryGroup.classList.add('form-group'); - let entryOptions = config[section][entry]['options']; - let innerGroup = ` - - '; - entryGroup.innerHTML = innerGroup; - entryGroup.getElementsByClassName('form-control')[0].required = required; - - } - sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); - } - } - let sectionButton = document.createElement('button'); - sectionButton.setAttribute('type', 'button'); - sectionButton.classList.add('list-group-item', 'list-group-item-action'); - sectionButton.appendChild(document.createTextNode(sectionTitle)); - sectionButton.id = section + '_button'; - sectionButton.setAttribute('data-toggle', 'collapse'); - sectionButton.setAttribute('data-target', '#' + section); - settingsList.appendChild(sectionButton); - settingsList.appendChild(sectionCollapse); - } - } - }; - req.send(); - settingsModal.show(); -} - -triggerTooltips(); - -function sendConfig(modalId, restart = false) { - let modal = document.getElementById(modalId); - modifiedConfig['restart-program'] = false; - if (restart) { - modifiedConfig['restart-program'] = true; - } - let send = JSON.stringify(modifiedConfig); - let req = new XMLHttpRequest(); - req.open("POST", "/modifyConfig", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - createModal(modalId, true).hide(); - if (modalId != 'settingsMenu') { - settingsModal.hide(); - } - } else if (restart) { - refreshModal.show(); - } - } - }; - req.send(send); -} - -document.getElementById('settingsSave').onclick = function() { - modifiedConfig = {}; - var restart_setting_changed = false; - var settings_changed = false; - - for (let section of config["order"]) { - for (let entry of config[section]["order"]) { - if (entry != 'meta') { - let entryID = section + '_' + entry; - let el = document.getElementById(entryID); - if (el.type == 'checkbox') { - var value = el.checked.toString(); - } else { - var value = el.value.toString(); - } - if (value != config[section][entry]['value'].toString()) { - if (!modifiedConfig.hasOwnProperty(section)) { - modifiedConfig[section] = {}; - } - modifiedConfig[section][entry] = value; - settings_changed = true; - if (config[section][entry]['requires_restart']) { - restart_setting_changed = true; - } - } - } - } - } - if (restart_setting_changed) { - document.getElementById('applyRestarts').onclick = function(){ sendConfig('restartModal'); }; - let restartButton = document.getElementById('applyAndRestart') - if (restartButton) { - restartButton.onclick = function(){ sendConfig('restartModal', restart=true); }; - } - settingsModal.hide(); - restartModal.show(); - } else if (settings_changed) { - sendConfig('settingsMenu'); - } else { - settingsModal.hide(); - } -} - -function setProfile(select) { - if (select.value == "") { - return; - } - let invite = select.id.replace("profile_", ""); - let req = new XMLHttpRequest(); - let send = { - "invite": invite, - "profile": select.value - }; - req.open("POST", "/setProfile", true); - req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status != 200) { - generateInvites(false); - } - }; - req.send(JSON.stringify(send)); -} - - -// Disable 'Generate' button if days, hours, minutes are all zero -function checkDuration() { - let boxVals = [document.getElementById("days").value, document.getElementById("hours").value, document.getElementById("minutes").value]; - let submit = document.getElementById("generateSubmit"); - if (boxVals[0] != 0 || boxVals[1] != 0 || boxVals[2] != 0) { - submit.disabled = false; - } else if (boxVals[0] == 0 && boxVals[1] == 0 && boxVals[2] == 0) { - submit.disabled = true; - } -} - -for (i of ["days", "hours", "minutes"]) { - document.getElementById(i).addEventListener("change", checkDuration); -} diff --git a/data/static/serialize.js b/data/static/serialize.js deleted file mode 100644 index 51aa656..0000000 --- a/data/static/serialize.js +++ /dev/null @@ -1,32 +0,0 @@ -function serializeForm(id) { - var form = document.getElementById(id); - var formData = {}; - for (var i = 0; i < form.elements.length; i++) { - var el = form.elements[i]; - if (el.type != 'submit') { - var name = el.name; - if (name == '') { - name = el.id; - }; - switch (el.type) { - case 'checkbox': - formData[name] = el.checked; - break; - case 'text': - case 'password': - case 'email': - case 'number': - formData[name] = el.value; - break; - case 'select-one': - let val = el.value; - if (!isNaN(val)) { - val = parseInt(val) - } - formData[name] = val; - break; - }; - }; - }; - return formData; -}; diff --git a/data/templates/admin.html b/data/templates/admin.html index 5e3684b..7b7e876 100644 --- a/data/templates/admin.html +++ b/data/templates/admin.html @@ -31,9 +31,9 @@ return ""; } {{ if .bs5 }} - const bsVersion = 5; + var bsVersion = 5; {{ else }} - const bsVersion = 4; + var bsVersion = 4; {{ end }} var cssFile = "{{ .cssFile }}"; var css = document.createElement('link'); @@ -52,8 +52,6 @@ } css.setAttribute('href', cssFile); document.head.appendChild(css); - // store whether ombi is enabled, 1 or 0. - var ombiEnabled = {{ .ombiEnabled }} {{ if not .bs5 }} @@ -315,7 +313,7 @@ - @@ -431,80 +429,24 @@ + {{ if .bs5 }} + + {{ else }} + + {{ end }} + + + + {{ if .ombiEnabled }} + + {{ end }} diff --git a/package-lock.json b/package-lock.json index 3377f44..fdd1e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,19 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/jquery": { + "version": "3.5.1", + "resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.1.tgz", + "integrity": "sha1-zrsFes9QccQOQ58w6EDFejDUBsM=", + "requires": { + "@types/sizzle": "*" + } + }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npm.taobao.org/@types/sizzle/download/@types/sizzle-2.3.2.tgz", + "integrity": "sha1-qBG4wY4rq6t9VCszZYh64uTZ3kc=" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1845,6 +1858,11 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npm.taobao.org/popper.js/download/popper.js-1.16.1.tgz", + "integrity": "sha1-KiI8s9x7YhPXQOQDcr5A3kPmWxs=" + }, "postcss": { "version": "7.0.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", diff --git a/package.json b/package.json index ca18fc4..eee2e05 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "homepage": "https://github.com/hrfee/jellyfin-accounts#readme", "dependencies": { + "@types/jquery": "^3.5.1", "autoprefixer": "^9.8.5", "bootstrap": "^5.0.0-alpha1", "bootstrap4": "npm:bootstrap@^4.5.0", diff --git a/ts/accounts.ts b/ts/accounts.ts index 89cca2b..77ba40a 100644 --- a/ts/accounts.ts +++ b/ts/accounts.ts @@ -1,32 +1,3 @@ -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'); - const checkCheckboxes = (): void => { const defaultsButton = document.getElementById('accountsTabSetDefaults'); const deleteButton = document.getElementById('accountsTabDelete'); @@ -46,15 +17,12 @@ const checkCheckboxes = (): void => { } } -const validateEmail = (email: string): boolean => { - const re = /\S+@\S+\.\S+/; - return re.test(email); -} +const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email); -const changeEmail = (icon: HTMLElement, id: string): void => { +function changeEmail(icon: HTMLElement, id: string): void { const iconContent = icon.outerHTML; icon.setAttribute('class', ''); - const entry: HTMLInputElement = icon.nextElementSibling; + const entry = icon.nextElementSibling as HTMLInputElement; const ogEmail = entry.value; entry.readOnly = false; entry.classList.remove('form-control-plaintext'); @@ -62,26 +30,26 @@ const changeEmail = (icon: HTMLElement, id: string): void => { if (ogEmail == "") { entry.placeholder = 'Address'; } - const tick = document.createElement('i'); - tick.outerHTML = ` + const tick = createEl(` - `; + `); tick.onclick = (): void => { const newEmail = entry.value; if (!validateEmail(newEmail) || newEmail == ogEmail) { return; } cross.remove(); - tick.outerHTML = ` + 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') { + if (this.status == 200 || this.status == 204) { entry.nextElementSibling.remove(); } else { entry.value = ogEmail; @@ -94,10 +62,9 @@ const changeEmail = (icon: HTMLElement, id: string): void => { entry.classList.add('form-control-plaintext'); entry.placeholder = ''; }; - const cross: HTMLElement = document.createElement('i'); - cross.outerHTML = ` + const cross = createEl(` - `; + `); cross.onclick = (): void => { tick.remove(); cross.remove(); @@ -114,7 +81,7 @@ const changeEmail = (icon: HTMLElement, id: string): void => { var jfUsers: Array; -const populateUsers = (): void => { +function populateUsers(): void { const acList = document.getElementById('accountsList'); acList.innerHTML = `
    @@ -157,7 +124,7 @@ const populateUsers = (): void => { }; _get("/getUsers", null, function (): void { - if (this.readyState == 4 && this.status == '200') { + if (this.readyState == 4 && this.status == 200) { jfUsers = this.response['users']; for (const user of jfUsers) { let tr = document.createElement('tr'); @@ -170,11 +137,32 @@ const populateUsers = (): void => { }); } +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 (const i in checkboxes) { + for (let i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = (this).checked; } + checkCheckboxes(); }; (document.getElementById('deleteModalNotify')).onclick = function (): void { @@ -186,11 +174,11 @@ const populateUsers = (): void => { } }; -(document.getElementById('accountsTabDelete')).onclick =function (): void { - const deleteButton: HTMLButtonElement = this; +(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 (const i in checkboxes){ + for (let i = 0; i < checkboxes.length; i++) { selected[i] = checkboxes[i].id.replace("select_", ""); } let title = " user"; @@ -203,16 +191,16 @@ const populateUsers = (): void => { msg += " of account deletion"; document.getElementById('deleteModalTitle').textContent = title; - const dmNotify: HTMLInputElement = document.getElementById('deleteModalNotify') + const dmNotify = document.getElementById('deleteModalNotify') as HTMLInputElement; dmNotify.checked = false; document.getElementById('deleteModalNotifyLabel').textContent = msg; - const dmReason: HTMLTextAreaElement = document.getElementById('deleteModalReason') + const dmReason = document.getElementById('deleteModalReason') as HTMLTextAreaElement; dmReason.value = ''; Unfocus(document.getElementById('deleteModalReasonBox')); - const dmSend: HTMLButtonElement = document.getElementById('deleteModalSend'); + const dmSend = document.getElementById('deleteModalSend') as HTMLButtonElement; dmSend.textContent = 'Delete'; dmSend.onclick = function (): void { - const button: HTMLButtonElement = this; + const button = this as HTMLButtonElement; const send = { 'users': selected, 'notify': dmNotify.checked, @@ -220,12 +208,12 @@ const populateUsers = (): void => { }; _post("/deleteUser", send, function (): void { if (this.readyState == 4) { - if (this.status == '500') { - if ("error" in req.reponse) { + if (this.status == 500) { + if ("error" in this.reponse) { button.textContent = 'Failed'; } else { button.textContent = 'Partial fail (check console)'; - console.log(req.response); + console.log(this.response); } setTimeout((): void => { Unfocus(deleteButton); @@ -248,7 +236,7 @@ const populateUsers = (): void => { (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 (const i in checkboxes){ + for (let i = 0; i < checkboxes.length; i++){ userIDs[i] = checkboxes[i].id.replace("select_", ""); } if (userIDs.length == 0) { @@ -281,8 +269,9 @@ const populateUsers = (): void => { }); (document.getElementById('newUserCreate')).onclick = function (): void { - const ogText = this.textContent; - this.innerHTML = ` + const button = this as HTMLButtonElement; + const ogText = button.textContent; + button.innerHTML = ` Creating... `; const email: string = (document.getElementById('newUserEmail')).value; @@ -299,23 +288,23 @@ const populateUsers = (): void => { 'password': password, 'email': email }; - const button: HTMLButtonElement = this; _post("/newUserAdmin", send, function (): void { if (this.readyState == 4) { rmAttr(button, 'btn-primary'); - if (this.status == '200') { + if (this.status == 200) { addAttr(button, 'btn-success'); button.textContent = 'Success'; setTimeout((): void => { rmAttr(button, 'btn-success'); - addAttr('btn-primary'); + addAttr(button, 'btn-primary'); button.textContent = ogText; newUserModal.hide(); }, 1000); + populateUsers(); } else { addAttr(button, 'btn-danger'); - if ("error" in req.response) { - button.textContent = req.response["error"]; + if ("error" in this.response) { + button.textContent = this.response["error"]; } else { button.textContent = 'Failed'; } @@ -324,6 +313,7 @@ const populateUsers = (): void => { addAttr(button, 'btn-primary'); button.textContent = ogText; }, 2000); + populateUsers(); } } }); @@ -337,3 +327,9 @@ const populateUsers = (): void => { } newUserModal.show(); }; + + + + + + diff --git a/ts/admin.ts b/ts/admin.ts new file mode 100644 index 0000000..1268826 --- /dev/null +++ b/ts/admin.ts @@ -0,0 +1,271 @@ +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 { + invitesEl: HTMLDivElement; + accountsEl: HTMLDivElement; + invitesTabButton: HTMLAnchorElement; + accountsTabButton: HTMLAnchorElement; + invites: () => void; + accounts: () => void; +} + +const tabs: TabSwitcher = { + invitesEl: document.getElementById('invitesTab') as HTMLDivElement, + accountsEl: document.getElementById('accountsTab') as HTMLDivElement, + invitesTabButton: document.getElementById('invitesTabButton') as HTMLAnchorElement, + accountsTabButton: document.getElementById('accountsTabButton') as HTMLAnchorElement, + invites: (): void => { + Unfocus(tabs.accountsEl); + Focus(tabs.invitesEl); + rmAttr(tabs.accountsTabButton, "active"); + addAttr(tabs.invitesTabButton, "active"); + }, + accounts: (): void => { + populateUsers(); + (document.getElementById('selectAll') as HTMLInputElement).checked = false; + checkCheckboxes(); + Unfocus(tabs.invitesEl); + Focus(tabs.accountsEl); + rmAttr(tabs.invitesTabButton, "active"); + addAttr(tabs.accountsTabButton, "active"); + } +}; + +tabs.invitesTabButton.onclick = tabs.invites; +tabs.accountsTabButton.onclick = tabs.accounts; + +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 settingsModal = createModal('settingsMenu'); +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; + const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement + let id = radio.id.replace("default_", ""); + let route = "/setDefaults"; + let data = { + "from": "user", + "id": id, + "homescreen": false + }; + if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'userTemplate') { + data["from"] = "template"; + } + if (users != "all") { + data["apply_to"] = users; + route = "/applySettings"; + } + if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) { + data["homescreen"] = true; + } + _post(route, 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/ts/animation.ts b/ts/animation.ts new file mode 100644 index 0000000..8b5b8c5 --- /dev/null +++ b/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/ts/bs4.ts b/ts/bs4.ts new file mode 100644 index 0000000..005e7af --- /dev/null +++ b/ts/bs4.ts @@ -0,0 +1,38 @@ +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 { + $("#settingsMenu").on("shown.bs.modal", (): void => { + const checkboxes = [].slice.call(document.getElementById('settingsMenu').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/ts/bs5.ts b/ts/bs5.ts new file mode 100644 index 0000000..f83bb47 --- /dev/null +++ b/ts/bs5.ts @@ -0,0 +1,36 @@ +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 { + (document.getElementById('settingsMenu') as HTMLButtonElement).addEventListener('shown.bs.modal', (): void => { + const checkboxes = [].slice.call(document.getElementById('settingsMenu').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/ts/invites.ts b/ts/invites.ts new file mode 100644 index 0000000..3a0f654 --- /dev/null +++ b/ts/invites.ts @@ -0,0 +1,362 @@ +// 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(CSS.escape(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']; + 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']; + } + 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']; + } + _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; + } + const invite = select.id.replace("profile_", ""); + const send = { + "invite": invite, + "profile": select.value + }; + _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/ts/ombi.ts b/ts/ombi.ts new file mode 100644 index 0000000..2217bf0 --- /dev/null +++ b/ts/ombi.ts @@ -0,0 +1,83 @@ +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"); + settingsModal.hide(); + ombiDefaultsModal.show(); + } + } + }); +}; + +(document.getElementById('storeOmbiDefaults') as HTMLButtonElement).onclick = function (): void { + let button = this as HTMLButtonElement; + button.disabled = true; + const ogHTML = button.innerHTML; + 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/ts/serialize.ts b/ts/serialize.ts new file mode 100644 index 0000000..e622a8b --- /dev/null +++ b/ts/serialize.ts @@ -0,0 +1,34 @@ +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 | number = (el as HTMLSelectElement).value; + if (+val != NaN) { + val = +val; + } + formData[name] = val; + break; + } + } + return formData; +} diff --git a/ts/settings.ts b/ts/settings.ts new file mode 100644 index 0000000..fae354a --- /dev/null +++ b/ts/settings.ts @@ -0,0 +1,206 @@ +var config: Object = {}; +var modifiedConfig: Object = {}; + +function sendConfig(modalID: string, restart?: boolean): void { + modifiedConfig["restart-program"] = restart; + _post("/modifyConfig", modifiedConfig, function (): void { + if (this.readyState == 4) { + if (this.status == 200 || this.status == 204) { + createModal(modalID, true).hide(); + if (modalID != "settingsMenu") { + settingsModal.hide(); + } + } + if (restart) { + refreshModal.show(); + } + } + }); +} + +(document.getElementById('openDefaultsWizard') as HTMLButtonElement).onclick = function (): void { + const button = this as HTMLButtonElement; + button.disabled = true; + const ogHTML = button.innerHTML; + button.innerHTML = ` + + Loading...`; + _get("/getUsers", null, function (): void { + if (this.readyState == 4) { + if (this.status == 200) { + jfUsers = this.response['users']; + populateRadios(); + button.disabled = false; + button.innerHTML = ogHTML; + const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement; + submitButton.disabled = false; + submitButton.textContent = 'Submit'; + addAttr(submitButton, "btn-primary"); + rmAttr(submitButton, "btn-danger"); + rmAttr(submitButton, "btn-success"); + document.getElementById('defaultsTitle').textContent = `New user defaults`; + document.getElementById('userDefaultsDescription').textContent = ` + Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.`; + 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 = (): void => storeDefaults('all'); + Focus(document.getElementById('defaultUserRadios')); + settingsModal.hide(); + userDefaultsModal.show(); + } + } + }); +}; + +(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => { + settingsModal.hide(); + aboutModal.show(); +}; + +(document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => _get("/getConfig", null, function (): void { + if (this.readyState == 4 && this.status == 200) { + const settingsList = document.getElementById('settingsList'); + settingsList.textContent = ''; + config = this.response; + for (const i in config["order"]) { + const section: string = config["order"][i] + const sectionCollapse = document.createElement('div') as HTMLDivElement; + addAttr(sectionCollapse, "collapse"); + 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 += ` + + `; + settingsList.appendChild(sectionCollapse); + } + settingsModal.show(); + } +}); + +(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void { + modifiedConfig = {}; + 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"]) { + if (!(section in modifiedConfig)) { + modifiedConfig[section] = {}; + } + modifiedConfig[section][entry] = val; + settingsChanged = true; + if (config[section][entry]["requires_restart"]) { + restartSettingsChanged = true; + } + } + } + } + if (restartSettingsChanged) { + (document.getElementById('applyRestarts') as HTMLButtonElement).onclick = (): void => sendConfig("restartModal"); + const restartButton = document.getElementById('applyAndRestart') as HTMLButtonElement; + if (restartButton) { + restartButton.onclick = (): void => sendConfig("restartModal", true); + } + settingsModal.hide(); + restartModal.show(); + } else if (settingsChanged) { + sendConfig("settingsMenu"); + } else { + settingsModal.hide(); + } +}; diff --git a/ts/tsconfig.json b/ts/tsconfig.json new file mode 100644 index 0000000..b380a8b --- /dev/null +++ b/ts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outDir": "../data/static", + "target": "es6", + "lib": ["dom", "es2017"], + "types": ["jquery"] + } +} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index a84c31d..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "lib": ["dom", "es2017"] - } -}