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
This commit is contained in:
Harvey Tindall 2020-09-21 22:03:20 +01:00
parent 73886fc037
commit 32b8ed4aa2
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
20 changed files with 1235 additions and 1732 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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

1
api.go
View File

@ -230,6 +230,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
}
}
app.jf.cacheExpiry = time.Now()
}
func (app *appContext) NewUser(gc *gin.Context) {

View File

@ -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 = `
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
<span class="sr-only">Saving...</span>
</div>`;
//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 = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>`;
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 = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
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 `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
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 = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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();
};

File diff suppressed because it is too large Load Diff

View File

@ -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;
};

View File

@ -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 }}
</script>
{{ if not .bs5 }}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
@ -315,7 +313,7 @@
<button type="button" class="btn btn-primary" id="openSettings">
Settings <i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-danger" id="logoutButton" style="display: none;">
<button type="button" class="btn btn-danger unfocused" id="logoutButton">
Logout <i class="fa fa-sign-out"></i>
</button>
</div>
@ -431,80 +429,24 @@
</div>
<script src="serialize.js"></script>
<script>
{{ if .bs5 }}
function createModal(id, find = false) {
let modal;
if (find) {
modal = bootstrap.Modal.getInstance(document.getElementById(id));
} else {
modal = new bootstrap.Modal(document.getElementById(id));
}
document.getElementById(id).addEventListener('shown.bs.modal', function () {
document.body.classList.add("modal-open");
});
return {
modal: modal,
show: function() {
let temp = this.modal.show();
return temp
},
hide: function() { return this.modal.hide(); }
};
}
{{ else }}
let send_to_addess_enabled = document.getElementById('send_to_address_enabled');
if (typeof(send_to_address_enabled) != 'undefined') {
send_to_address_enabled.classList.remove('form-check-input');
}
let multiUseEnabled = document.getElementById('multiUseEnabled');
if (typeof(multiUseEnabled) != 'undefined') {
multiUseEnabled.classList.remove('form-check-input');
}
function createModal(id, find = false) {
$('#' + id).on('shown.bs.modal', function () {
document.body.classList.add("modal-open");
});
return {
show: function() {
let temp = $('#' + id).modal('show');
return temp
},
hide: function() {
return $('#' + id).modal('hide');
}
};
}
{{ end }}
function triggerTooltips() {
{{ if .bs5 }}
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
{{ else }}
$('#settingsMenu').on('shown.bs.modal', function() {
{{ end }}
// Hacky way to ensure anything dependent on checkbox state is disabled if necessary by just clicking them
let checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
for (checkbox of checkboxes) {
checkbox.click();
checkbox.click();
}
let tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map(function(el) {
{{ if .bs5 }}
return new bootstrap.Tooltip(el);
{{ else }}
return $(el).tooltip();
{{ end }}
});
});
}
{{ if .notifications }}
const notifications_enabled = true;
var notifications_enabled = true;
{{ else }}
const notifications_enabled = false;
var notifications_enabled = false;
{{ end }}
</script>
{{ if .bs5 }}
<script src="bs5.js"></script>
{{ else }}
<script src="bs4.js"></script>
{{ end }}
<script src="animation.js"></script>
<script src="accounts.js"></script>
<script src="invites.js"></script>
<script src="admin.js"></script>
<script src="settings.js"></script>
{{ if .ombiEnabled }}
<script src="ombi.js"></script>
{{ end }}
</body>
</html>

18
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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(`
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
`;
`);
tick.onclick = (): void => {
const newEmail = entry.value;
if (!validateEmail(newEmail) || newEmail == ogEmail) {
return;
}
cross.remove();
tick.outerHTML = `
const spinner = createEl(`
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
<span class="sr-only">Saving...</span>
</div>
`;
`);
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(`
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
`;
`);
cross.onclick = (): void => {
tick.remove();
cross.remove();
@ -114,7 +81,7 @@ const changeEmail = (icon: HTMLElement, id: string): void => {
var jfUsers: Array<Object>;
const populateUsers = (): void => {
function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
@ -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 = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
for (const i in checkboxes) {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = (<HTMLInputElement>this).checked;
}
checkCheckboxes();
};
(<HTMLInputElement>document.getElementById('deleteModalNotify')).onclick = function (): void {
@ -186,11 +174,11 @@ const populateUsers = (): void => {
}
};
(<HTMLButtonElement>document.getElementById('accountsTabDelete')).onclick =function (): void {
const deleteButton: HTMLButtonElement = this;
(<HTMLButtonElement>document.getElementById('accountsTabDelete')).onclick = function (): void {
const deleteButton = this as HTMLButtonElement;
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let selected: Array<string> = 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 => {
(<HTMLButtonElement>document.getElementById('accountsTabSetDefaults')).onclick = function (): void {
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let userIDs: Array<string> = 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 => {
});
(<HTMLButtonElement>document.getElementById('newUserCreate')).onclick = function (): void {
const ogText = this.textContent;
this.innerHTML = `
const button = this as HTMLButtonElement;
const ogText = button.textContent;
button.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...
`;
const email: string = (<HTMLInputElement>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();
};

271
ts/admin.ts Normal file
View File

@ -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
<i class="fa fa-circle circle" style="color: ${buttonColor}; margin-left: 0.4rem;" id="fakeButton"></i>
`;
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<string>;
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 = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
Loading...`;
login(details["username"], details["password"], true, button);
return false;
};
function storeDefaults(users: string | Array<string>): void {
// not sure if this does anything, but w/e
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'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;
}
});
};

79
ts/animation.ts Normal file
View File

@ -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<HTMLLinkElement> = 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");
}
};

38
ts/bs4.ts Normal file
View File

@ -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();
});
});
}

36
ts/bs5.ts Normal file
View File

@ -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);
});
});
}

362
ts/invites.ts Normal file
View File

@ -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<Array<string>>;
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<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
}
uB += `</ul>`
}
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 = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
`;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="setProfile(this)">
`;
let match = false;
for (const i in availableProfiles) {
let selected = "";
if (availableProfiles[i] == invite.profile) {
selected = "selected";
match = true;
}
profiles += `<option value="${availableProfiles[i]}" ${selected}>${availableProfiles[i]}</option>`;
}
if (!match) {
profiles += `<option value="" selected></option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
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 = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
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<HTMLInputElement> = [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 = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
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<number> = [+(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<string> = ["days", "hours", "minutes"];
for (const i in nE) {
document.getElementById(nE[i]).addEventListener("change", checkDuration);
}

83
ts/ombi.ts Normal file
View File

@ -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 =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'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 = `
<input class="form-check-input" type="radio" name="ombiRadios" id="ombiDefault_${user['id']}" ${checked}>
<label class="form-check-label" for="ombiDefault_${user['id']}">${user['name']}</label>
`;
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 =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'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);
}
}
});
};

34
ts/serialize.ts Normal file
View File

@ -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;
}

206
ts/settings.ts Normal file
View File

@ -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 = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
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 = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
</div>
`;
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 += ` <sup class="text-danger">*</sup>`;
required = true;
}
if (config[section][entry]["requires_restart"]) {
entryName += ` <sup class="text-danger">R</sup>`;
}
if ("description" in config[section][entry]) {
entryName +=`
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
`;
}
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 = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(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 = `
<label for="${section}_${entry}">${entryName}</label>
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" data-toggle="collapse" data-target="#${section}">${title}</button>
`;
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();
}
};

8
ts/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"outDir": "../data/static",
"target": "es6",
"lib": ["dom", "es2017"],
"types": ["jquery"]
}
}

View File

@ -1,6 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "es2017"]
}
}