mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 09:00:14 +00:00
Merge pull request #28 from hrfee/bs5
Add themes, Bootstrap 5 support, bump to 0.3.0
This commit is contained in:
commit
9da3832e3a
7
.gitignore
vendored
7
.gitignore
vendored
@ -4,13 +4,16 @@ MANIFEST.in
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
test.txt
|
test.txt
|
||||||
jellyfin_accounts/data/node_modules/
|
node_modules/
|
||||||
jellyfin_accounts/data/config-default.ini
|
jellyfin_accounts/data/config-default.ini
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
pw-reset/
|
pw-reset/
|
||||||
jfa/
|
jfa/
|
||||||
colors.txt
|
colors.txt
|
||||||
theme.css
|
theme.css
|
||||||
jellyfin_accounts/data/static/bootstrap-jf.css
|
jellyfin_accounts/__pycache__/
|
||||||
old/
|
old/
|
||||||
.jf-accounts/
|
.jf-accounts/
|
||||||
|
requirements.txt
|
||||||
|
package-lock.json
|
||||||
|
video/
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/jellyfin-accounts-banner-wide.svg)
|
# ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/bs5/images/jellyfin-accounts-banner-wide.svg)
|
||||||
|
|
||||||
A basic account management system for [Jellyfin](https://github.com/jellyfin/jellyfin).
|
A basic account management system for [Jellyfin](https://github.com/jellyfin/jellyfin).
|
||||||
* Provides a web interface for creating invite codes, and a simple account creation form
|
* Provides a web interface for creating/sending invites
|
||||||
* Sends out emails when a user requests a password reset
|
* Sends out emails when a user requests a password reset
|
||||||
* Uses a basic python jellyfin API client for communication with the server.
|
* Uses a basic python jellyfin API client for communication with the server.
|
||||||
* Uses [Flask](https://github.com/pallets/flask), [HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth), [itsdangerous](https://github.com/pallets/itsdangerous), and [Waitress](https://github.com/Pylons/waitress)
|
* Uses [Flask](https://github.com/pallets/flask), [HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth), [itsdangerous](https://github.com/pallets/itsdangerous), and [Waitress](https://github.com/Pylons/waitress)
|
||||||
* Frontend uses [Bootstrap](https://getbootstrap.com), [jQuery](https://jquery.com) and [jQuery-serialize-object](https://github.com/macek/jquery-serialize-object)
|
* Frontend uses [Bootstrap](https://v5.getbootstrap.com)
|
||||||
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
||||||
## Interface
|
## Interface
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -9,12 +9,14 @@ server = http://jellyfin.local:8096
|
|||||||
public_server = https://jellyf.in:443
|
public_server = https://jellyf.in:443
|
||||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||||
client = jf-accounts
|
client = jf-accounts
|
||||||
version = 0.2.5
|
version = 0.3.0
|
||||||
device = jf-accounts
|
device = jf-accounts
|
||||||
device_id = jf-accounts-0.2.5
|
device_id = jf-accounts-0.3.0
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; settings related to the ui and program functionality.
|
; settings related to the ui and program functionality.
|
||||||
|
; choose the look of jellyfin-accounts.
|
||||||
|
theme = Jellyfin (Dark)
|
||||||
; set 0.0.0.0 to run on localhost
|
; set 0.0.0.0 to run on localhost
|
||||||
host = 0.0.0.0
|
host = 0.0.0.0
|
||||||
port = 8056
|
port = 8056
|
||||||
@ -33,6 +35,8 @@ contact_message = Need help? contact me.
|
|||||||
help_message = Enter your details to create an account.
|
help_message = Enter your details to create an account.
|
||||||
; displayed when a user creates an account
|
; displayed when a user creates an account
|
||||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||||
|
; use bootstrap 5 (currently in alpha). this also removes the need for jquery, so the page should load faster.
|
||||||
|
bs5 = false
|
||||||
|
|
||||||
[password_validation]
|
[password_validation]
|
||||||
; password validation (minimum length, etc.)
|
; password validation (minimum length, etc.)
|
||||||
|
BIN
images/admin.png
BIN
images/admin.png
Binary file not shown.
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 74 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 106 KiB |
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 6.7 MiB |
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
__version__ = "0.2.6"
|
__version__ = "0.3.0"
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import configparser
|
import configparser
|
||||||
@ -142,6 +142,8 @@ def load_config(config_path, data_dir):
|
|||||||
or config["jellyfin"]["public_server"] == ""
|
or config["jellyfin"]["public_server"] == ""
|
||||||
):
|
):
|
||||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||||
|
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||||
|
config["ui"]["bs5"] = "false"
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@ -185,31 +187,39 @@ data_store = JSONStorage(
|
|||||||
config["files"]["user_configuration"],
|
config["files"]["user_configuration"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if config.getboolean("ui", "bs5"):
|
||||||
def default_css():
|
css_file = "bs5-jf.css"
|
||||||
css = {}
|
log.debug("Using Bootstrap 5")
|
||||||
css[
|
else:
|
||||||
"href"
|
css_file = "bs4-jf.css"
|
||||||
] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
|
||||||
css[
|
|
||||||
"integrity"
|
|
||||||
] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
|
||||||
css["crossorigin"] = "anonymous"
|
|
||||||
return css
|
|
||||||
|
|
||||||
|
|
||||||
css = {}
|
with open(config_base_path, "r") as f:
|
||||||
css = default_css()
|
themes = json.load(f)["ui"]["theme"]
|
||||||
if "custom_css" in config["files"]:
|
|
||||||
|
theme_options = themes["options"]
|
||||||
|
|
||||||
|
if "theme" not in config["ui"] or config["ui"]["theme"] not in theme_options:
|
||||||
|
config["ui"]["theme"] = themes["value"]
|
||||||
|
|
||||||
|
if config.getboolean("ui", "bs5"):
|
||||||
|
num = 5
|
||||||
|
else:
|
||||||
|
num = 4
|
||||||
|
|
||||||
|
current_theme = config["ui"]["theme"]
|
||||||
|
|
||||||
|
if "Bootstrap" in current_theme:
|
||||||
|
css_file = f"bs{num}.css"
|
||||||
|
elif "Jellyfin" in current_theme:
|
||||||
|
css_file = f"bs{num}-jf.css"
|
||||||
|
elif "Custom" in current_theme and "custom_css" in config["files"]:
|
||||||
if config["files"]["custom_css"] != "":
|
if config["files"]["custom_css"] != "":
|
||||||
try:
|
try:
|
||||||
shutil.copy(
|
css_path = Path(config["files"]["custom_css"])
|
||||||
config["files"]["custom_css"], (local_dir / "static" / "bootstrap.css")
|
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||||
)
|
log.debug('Loaded custom CSS "{css_path.name}"')
|
||||||
log.debug("Loaded custom CSS")
|
css_file = css_path.name
|
||||||
css["href"] = "/bootstrap.css"
|
|
||||||
css["integrity"] = ""
|
|
||||||
css["crossorigin"] = ""
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log.error(
|
log.error(
|
||||||
f'Custom CSS {config["files"]["custom_css"]} not found, using default.'
|
f'Custom CSS {config["files"]["custom_css"]} not found, using default.'
|
||||||
|
@ -70,6 +70,19 @@
|
|||||||
"name": "General",
|
"name": "General",
|
||||||
"description": "Settings related to the UI and program functionality."
|
"description": "Settings related to the UI and program functionality."
|
||||||
},
|
},
|
||||||
|
"theme": {
|
||||||
|
"name": "Look",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Bootstrap (Light)",
|
||||||
|
"Jellyfin (Dark)",
|
||||||
|
"Custom CSS"
|
||||||
|
],
|
||||||
|
"value": "Jellyfin (Dark)",
|
||||||
|
"description": "Choose the look of jellyfin-accounts."
|
||||||
|
},
|
||||||
"host": {
|
"host": {
|
||||||
"name": "Address",
|
"name": "Address",
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -150,6 +163,14 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||||
"description": "Displayed when a user creates an account"
|
"description": "Displayed when a user creates an account"
|
||||||
|
},
|
||||||
|
"bs5": {
|
||||||
|
"name": "Use Bootstrap 5",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"password_validation": {
|
"password_validation": {
|
||||||
@ -208,7 +229,7 @@
|
|||||||
"no_username": {
|
"no_username": {
|
||||||
"name": "Use email addresses as username",
|
"name": "Use email addresses as username",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": false,
|
"requires_restart": true,
|
||||||
"depends_true": "method",
|
"depends_true": "method",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": false,
|
"value": false,
|
||||||
|
@ -1,665 +0,0 @@
|
|||||||
function parseInvite(invite, empty = false) {
|
|
||||||
if (empty === true) {
|
|
||||||
return ["None", "", "1"]
|
|
||||||
} else {
|
|
||||||
var i = ["", "", "0", invite['email']];
|
|
||||||
i[0] = invite['code'];
|
|
||||||
if (invite['hours'] == 0) {
|
|
||||||
i[1] = invite['minutes'] + 'm';
|
|
||||||
} else if (invite['minutes'] == 0) {
|
|
||||||
i[1] = invite['hours'] + 'h';
|
|
||||||
} else {
|
|
||||||
i[1] = invite['hours'] + 'h ' + invite['minutes'] + 'm';
|
|
||||||
}
|
|
||||||
i[1] = "Expires in " + i[1] + " ";
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function addItem(invite) {
|
|
||||||
var links = document.getElementById('invites');
|
|
||||||
var listItem = document.createElement('li');
|
|
||||||
listItem.id = invite[0]
|
|
||||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
|
||||||
var listCode = document.createElement('div');
|
|
||||||
listCode.classList.add('d-flex', 'align-items-center', 'text-monospace');
|
|
||||||
var codeLink = document.createElement('a');
|
|
||||||
codeLink.setAttribute('style', 'margin-right: 2%;');
|
|
||||||
codeLink.appendChild(document.createTextNode(invite[0].replace(/-/g, '‑')));
|
|
||||||
listCode.appendChild(codeLink);
|
|
||||||
listItem.appendChild(listCode);
|
|
||||||
var listRight = document.createElement('div');
|
|
||||||
listText = document.createElement('span');
|
|
||||||
listText.id = invite[0] + '_expiry'
|
|
||||||
listText.appendChild(document.createTextNode(invite[1]));
|
|
||||||
listRight.appendChild(listText);
|
|
||||||
if (invite[2] == 0) {
|
|
||||||
var inviteCode = window.location.href + 'invite/' + invite[0];
|
|
||||||
codeLink.href = inviteCode;
|
|
||||||
// listCode.appendChild(document.createTextNode(" "));
|
|
||||||
var codeCopy = document.createElement('i');
|
|
||||||
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
|
||||||
codeCopy.classList.add('fa', 'fa-clipboard');
|
|
||||||
listCode.appendChild(codeCopy);
|
|
||||||
if (typeof(invite[3]) != 'undefined') {
|
|
||||||
var sentTo = document.createElement('span');
|
|
||||||
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
|
|
||||||
if (invite[3].includes('Failed to send to')) {
|
|
||||||
sentTo.appendChild(document.createTextNode(invite[3]));
|
|
||||||
} else {
|
|
||||||
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
|
|
||||||
}
|
|
||||||
listCode.appendChild(sentTo);
|
|
||||||
};
|
|
||||||
var listDelete = document.createElement('button');
|
|
||||||
listDelete.onclick = function(){deleteInvite(invite[0])};
|
|
||||||
listDelete.classList.add('btn', 'btn-outline-danger');
|
|
||||||
listDelete.appendChild(document.createTextNode('Delete'));
|
|
||||||
listRight.appendChild(listDelete);
|
|
||||||
};
|
|
||||||
listItem.appendChild(listRight);
|
|
||||||
links.appendChild(listItem);
|
|
||||||
};
|
|
||||||
function updateInvite(invite) {
|
|
||||||
var expiry = document.getElementById(invite[0] + '_expiry');
|
|
||||||
expiry.textContent = invite[1];
|
|
||||||
}
|
|
||||||
function removeInvite(code) {
|
|
||||||
var item = document.getElementById(code);
|
|
||||||
item.parentNode.removeChild(item);
|
|
||||||
}
|
|
||||||
function generateInvites(empty = false) {
|
|
||||||
// document.getElementById('invites').textContent = '';
|
|
||||||
if (empty === false) {
|
|
||||||
$.ajax('/getInvites', {
|
|
||||||
type : 'GET',
|
|
||||||
dataType : 'json',
|
|
||||||
contentType: 'json',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
data: { get_param: 'value' },
|
|
||||||
complete: function(response) {
|
|
||||||
var data = JSON.parse(response['responseText']);
|
|
||||||
if (data['invites'].length == 0) {
|
|
||||||
document.getElementById('invites').textContent = '';
|
|
||||||
addItem(parseInvite([], true));
|
|
||||||
} else {
|
|
||||||
data['invites'].forEach(function(invite) {
|
|
||||||
var match = false;
|
|
||||||
var items = document.getElementById('invites').children;
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].id == invite['code']) {
|
|
||||||
match = true;
|
|
||||||
updateInvite(parseInvite(invite));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (match == false) {
|
|
||||||
addItem(parseInvite(invite));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
var items = document.getElementById('invites').children;
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
var exists = false;
|
|
||||||
data['invites'].forEach(function(invite) {
|
|
||||||
if (items[i].id == invite['code']) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (exists == false) {
|
|
||||||
removeInvite(items[i].id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (empty === true) {
|
|
||||||
document.getElementById('invites').textContent = '';
|
|
||||||
addItem(parseInvite([], true));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
function deleteInvite(code) {
|
|
||||||
var send = JSON.stringify({ "code": code });
|
|
||||||
$.ajax('/deleteInvite', {
|
|
||||||
data : send,
|
|
||||||
contentType : 'application/json',
|
|
||||||
type : 'POST',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
success: function() { generateInvites(); },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
function addOptions(le, sel) {
|
|
||||||
for (v = 0; v <= le; v++) {
|
|
||||||
var opt = document.createElement('option');
|
|
||||||
opt.appendChild(document.createTextNode(v))
|
|
||||||
opt.value = v
|
|
||||||
sel.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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$("form#inviteForm").submit(function() {
|
|
||||||
var button = document.getElementById('generateSubmit');
|
|
||||||
button.disabled = true;
|
|
||||||
button.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
var send_object = $("form#inviteForm").serializeObject();
|
|
||||||
if (document.getElementById('send_to_address') != null) {
|
|
||||||
if (document.getElementById('send_to_address_enabled').checked) {
|
|
||||||
send_object['email'] = document.getElementById('send_to_address').value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var send = JSON.stringify(send_object);
|
|
||||||
$.ajax('/generateInvite', {
|
|
||||||
data : send,
|
|
||||||
contentType : 'application/json',
|
|
||||||
type : 'POST',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
success: function() {
|
|
||||||
button.textContent = 'Generate';
|
|
||||||
button.disabled = false;
|
|
||||||
generateInvites();
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$("form#loginForm").submit(function() {
|
|
||||||
window.token = "";
|
|
||||||
var details = $("form#loginForm").serializeObject();
|
|
||||||
var errorArea = document.getElementById('loginErrorArea');
|
|
||||||
errorArea.textContent = '';
|
|
||||||
var button = document.getElementById('loginSubmit');
|
|
||||||
button.disabled = true;
|
|
||||||
button.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
$.ajax('/getToken', {
|
|
||||||
type : 'GET',
|
|
||||||
dataType : 'json',
|
|
||||||
contentType: 'json',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
|
||||||
},
|
|
||||||
data: { get_param: 'value' },
|
|
||||||
complete: function(data) {
|
|
||||||
if (data['status'] == 401) {
|
|
||||||
button.disabled = false;
|
|
||||||
button.textContent = 'Login';
|
|
||||||
var wrongPassword = document.createElement('div');
|
|
||||||
wrongPassword.classList.add('alert', 'alert-danger');
|
|
||||||
wrongPassword.setAttribute('role', 'alert');
|
|
||||||
wrongPassword.appendChild(document.createTextNode('Incorrect username or password.'));
|
|
||||||
errorArea.appendChild(wrongPassword);
|
|
||||||
} else {
|
|
||||||
window.token = JSON.parse(data['responseText'])['token'];
|
|
||||||
generateInvites();
|
|
||||||
var interval = setInterval(function() { generateInvites(); }, 60 * 1000);
|
|
||||||
var hour = document.getElementById('hours');
|
|
||||||
addOptions(24, hour);
|
|
||||||
hour.selected = "0";
|
|
||||||
var minutes = document.getElementById('minutes');
|
|
||||||
addOptions(59, minutes);
|
|
||||||
minutes.selected = "30";
|
|
||||||
$('#login').modal('hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
document.getElementById('openDefaultsWizard').onclick = function () {
|
|
||||||
this.disabled = true;
|
|
||||||
this.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
$.ajax('getUsers', {
|
|
||||||
type : 'GET',
|
|
||||||
dataType : 'json',
|
|
||||||
contentType : 'json',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
complete : function(data) {
|
|
||||||
if (data['status'] == 200) {
|
|
||||||
var radioList = document.getElementById('defaultUserRadios');
|
|
||||||
radioList.textContent = '';
|
|
||||||
if (document.getElementById('setDefaultUser')) {
|
|
||||||
document.getElementById('setDefaultUser').remove();
|
|
||||||
};
|
|
||||||
var users = data['responseJSON']['users'];
|
|
||||||
for (var i = 0; i < users.length; i++) {
|
|
||||||
var user = users[i]
|
|
||||||
var radio = document.createElement('div');
|
|
||||||
radio.classList.add('radio');
|
|
||||||
if (i == 0) {
|
|
||||||
var checked = 'checked';
|
|
||||||
} else {
|
|
||||||
var checked = '';
|
|
||||||
};
|
|
||||||
radio.innerHTML =
|
|
||||||
'<label><input type="radio" name="defaultRadios" id="default_' +
|
|
||||||
user['name'] + '" style="margin-right: 1rem;"' + checked + '>' +
|
|
||||||
user['name'] + '</label>';
|
|
||||||
radioList.appendChild(radio);
|
|
||||||
}
|
|
||||||
var button = document.getElementById('openDefaultsWizard');
|
|
||||||
button.disabled = false;
|
|
||||||
button.innerHTML = 'Set new account defaults';
|
|
||||||
var 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');
|
|
||||||
};
|
|
||||||
$('#settingsMenu').modal('hide');
|
|
||||||
$('#userDefaults').modal('show');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
document.getElementById('storeDefaults').onclick = function () {
|
|
||||||
this.disabled = true;
|
|
||||||
this.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
var button = document.getElementById('storeDefaults');
|
|
||||||
var radios = document.getElementsByName('defaultRadios');
|
|
||||||
for (var i = 0; i < radios.length; i++) {
|
|
||||||
if (radios[i].checked) {
|
|
||||||
var data = {'username':radios[i].id.slice(8), 'homescreen':false};
|
|
||||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
|
||||||
data['homescreen'] = true;
|
|
||||||
}
|
|
||||||
$.ajax('/setDefaults', {
|
|
||||||
data : JSON.stringify(data),
|
|
||||||
contentType : 'application/json',
|
|
||||||
type : 'POST',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
success: function() {
|
|
||||||
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(){$('#userDefaults').modal('hide');}, 1000);
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
button.textContent = 'Failed';
|
|
||||||
config_base_path = local_dir / "config-base.json"
|
|
||||||
button.classList.remove('btn-primary');
|
|
||||||
button.classList.add('btn-danger');
|
|
||||||
setTimeout(function(){
|
|
||||||
var button = document.getElementById('storeDefaults');
|
|
||||||
button.textContent = 'Submit';
|
|
||||||
button.classList.remove('btn-danger');
|
|
||||||
button.classList.add('btn-primary');
|
|
||||||
button.disabled = false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.getElementById('openUsers').onclick = function () {
|
|
||||||
this.disabled = true;
|
|
||||||
this.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'Loading...';
|
|
||||||
$.ajax('/getUsers', {
|
|
||||||
type : 'GET',
|
|
||||||
dataType : 'json',
|
|
||||||
contentType: 'json',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
data: { get_param: 'value' },
|
|
||||||
complete : function(data) {
|
|
||||||
if (data['status'] == 200) {
|
|
||||||
var list = document.getElementById('userList');
|
|
||||||
list.textContent = '';
|
|
||||||
if (document.getElementById('saveUsers')) {
|
|
||||||
document.getElementById('saveUsers').remove();
|
|
||||||
};
|
|
||||||
var users = data['responseJSON']['users'];
|
|
||||||
for (var i = 0; i < users.length; i++) {
|
|
||||||
var user = users[i]
|
|
||||||
var entry = document.createElement('p');
|
|
||||||
entry.id = 'user_' + user['name'];
|
|
||||||
entry.appendChild(document.createTextNode(user['name']));
|
|
||||||
var address = document.createElement('span');
|
|
||||||
address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
|
|
||||||
address.classList.add('addressText');
|
|
||||||
address.id = 'address_' + user['email'];
|
|
||||||
if (typeof(user['email']) != 'undefined') {
|
|
||||||
address.appendChild(document.createTextNode(user['email']));
|
|
||||||
};
|
|
||||||
var editButton = document.createElement('i');
|
|
||||||
editButton.classList.add('fa', 'fa-edit');
|
|
||||||
editButton.onclick = function() {
|
|
||||||
this.classList.remove('fa', 'fa-edit');
|
|
||||||
var input = document.createElement('input');
|
|
||||||
input.setAttribute('type', 'email');
|
|
||||||
input.setAttribute('style', 'margin-left: 2%; color: grey;');
|
|
||||||
var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
|
|
||||||
if (addressElement.textContent != '') {
|
|
||||||
input.value = addressElement.textContent;
|
|
||||||
} else {
|
|
||||||
input.placeholder = 'Email Address';
|
|
||||||
};
|
|
||||||
this.parentNode.replaceChild(input, addressElement);
|
|
||||||
if (document.getElementById('saveUsers') == null) {
|
|
||||||
var footer = document.getElementById('userFooter')
|
|
||||||
var saveUsers = document.createElement('input');
|
|
||||||
saveUsers.classList.add('btn', 'btn-primary');
|
|
||||||
saveUsers.setAttribute('type', 'button');
|
|
||||||
saveUsers.value = 'Save Changes';
|
|
||||||
saveUsers.id = 'saveUsers';
|
|
||||||
saveUsers.onclick = function() {
|
|
||||||
var send = {}
|
|
||||||
var entries = document.getElementById('userList').children;
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
|
||||||
var entry = entries[i];
|
|
||||||
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
|
||||||
var name = entry.id.replace(/user_/g, '')
|
|
||||||
var address = entry.getElementsByTagName('input')[0].value;
|
|
||||||
send[name] = address
|
|
||||||
};
|
|
||||||
};
|
|
||||||
send = JSON.stringify(send);
|
|
||||||
$.ajax('/modifyUsers', {
|
|
||||||
data : send,
|
|
||||||
contentType : 'application/json',
|
|
||||||
type : 'POST',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
success: function() { $('#users').modal('hide'); },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
footer.appendChild(saveUsers);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
entry.appendChild(address);
|
|
||||||
entry.appendChild(editButton);
|
|
||||||
list.appendChild(entry);
|
|
||||||
};
|
|
||||||
var button = document.getElementById('openUsers');
|
|
||||||
button.disabled = false;
|
|
||||||
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
|
||||||
$('#settingsMenu').modal('hide');
|
|
||||||
$('#users').modal('show');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
generateInvites(empty = true);
|
|
||||||
$("#login").modal('show');
|
|
||||||
|
|
||||||
var config = {};
|
|
||||||
var modifiedConfig = {};
|
|
||||||
|
|
||||||
document.getElementById('openSettings').onclick = function () {
|
|
||||||
restart_setting_changed = false;
|
|
||||||
$.ajax('getConfig', {
|
|
||||||
type : 'GET',
|
|
||||||
dataType : 'json',
|
|
||||||
contentType : 'json',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
complete : function(data) {
|
|
||||||
if (data['status'] == 200) {
|
|
||||||
var settingsList = document.getElementById('settingsList');
|
|
||||||
settingsList.textContent = '';
|
|
||||||
config = data['responseJSON'];
|
|
||||||
for (var section of Object.keys(config)) {
|
|
||||||
var sectionCollapse = document.createElement('div');
|
|
||||||
sectionCollapse.classList.add('collapse');
|
|
||||||
sectionCollapse.id = section;
|
|
||||||
|
|
||||||
var sectionTitle = config[section]['meta']['name'];
|
|
||||||
var sectionDescription = config[section]['meta']['description'];
|
|
||||||
var entryListID = section + '_entryList';
|
|
||||||
var sectionFooter = section + '_footer';
|
|
||||||
|
|
||||||
var innerCollapse = `
|
|
||||||
<div class="card card-body">
|
|
||||||
<small class="text-muted">${sectionDescription}</small>
|
|
||||||
<div class="${entryListID}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
sectionCollapse.innerHTML = innerCollapse;
|
|
||||||
|
|
||||||
for (var entry of Object.keys(config[section])) {
|
|
||||||
if (entry != 'meta') {
|
|
||||||
var entryName = config[section][entry]['name'];
|
|
||||||
var 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 (config[section][entry].hasOwnProperty('description')) {
|
|
||||||
var tooltip = `
|
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
entryName += ' ';
|
|
||||||
entryName += tooltip;
|
|
||||||
};
|
|
||||||
var entryValue = config[section][entry]['value'];
|
|
||||||
var entryType = config[section][entry]['type'];
|
|
||||||
var 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 = `
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}">
|
|
||||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<label for="${section}_${entry}">${entryName}</label>
|
|
||||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}">
|
|
||||||
`;
|
|
||||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
|
||||||
} else if (entryType == 'select') {
|
|
||||||
entryGroup.classList.add('form-group');
|
|
||||||
var entryOptions = config[section][entry]['options'];
|
|
||||||
var innerGroup = `
|
|
||||||
<label for="${section}_${entry}">${entryName}</label>
|
|
||||||
<select class="form-control" id="${section}_${entry}">
|
|
||||||
`;
|
|
||||||
for (var i = 0; i < entryOptions.length; i++) {
|
|
||||||
if (entryOptions[i] == entryValue) {
|
|
||||||
var selected = 'selected';
|
|
||||||
} else {
|
|
||||||
var selected = '';
|
|
||||||
}
|
|
||||||
innerGroup += `
|
|
||||||
<option value="${entryOptions[i]}" ${selected}>${entryOptions[i]}</option>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
innerGroup += '</select>';
|
|
||||||
entryGroup.innerHTML = innerGroup;
|
|
||||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
|
||||||
|
|
||||||
};
|
|
||||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var 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);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
$('#settingsMenu').modal('show');
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
|
||||||
$("a[data-toggle='tooltip']").each(function (i, obj) {
|
|
||||||
$(obj).tooltip();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendConfig(modalId) {
|
|
||||||
var modal = document.getElementById(modalId);
|
|
||||||
var send = JSON.stringify(modifiedConfig);
|
|
||||||
$.ajax('/modifyConfig', {
|
|
||||||
data : send,
|
|
||||||
contentType : 'application/json',
|
|
||||||
type : 'POST',
|
|
||||||
xhrFields : {
|
|
||||||
withCredentials: true
|
|
||||||
},
|
|
||||||
beforeSend : function (xhr) {
|
|
||||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
|
||||||
},
|
|
||||||
success: function() {
|
|
||||||
$('#' + modalId).modal('hide');
|
|
||||||
if (modalId != 'settingsMenu') {
|
|
||||||
$('#settingsMenu').modal('hide');
|
|
||||||
};
|
|
||||||
},
|
|
||||||
fail: function(xhr, textStatus, errorThrown) {
|
|
||||||
var footer = modal.getElementsByClassName('modal-dialog')[0].getElementsByClassName('modal-content')[0].getElementsByClassName('modal-footer')[0];
|
|
||||||
var alert = document.createElement('div');
|
|
||||||
alert.classList.add('alert', 'alert-danger');
|
|
||||||
alert.setAttribute('role', 'alert');
|
|
||||||
alert.appendChild(document.createTextNode('Error: ' + errorThrown));
|
|
||||||
footer.appendChild(alert);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('settingsSave').onclick = function() {
|
|
||||||
modifiedConfig = {};
|
|
||||||
var restart_setting_changed = false;
|
|
||||||
var settings_changed = false;
|
|
||||||
|
|
||||||
for (var section of Object.keys(config)) {
|
|
||||||
for (var entry of Object.keys(config[section])) {
|
|
||||||
if (entry != 'meta') {
|
|
||||||
var entryID = section + '_' + entry;
|
|
||||||
var 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) {
|
|
||||||
if (restart_setting_changed) {
|
|
||||||
document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
|
|
||||||
$('#settingsMenu').modal('hide');
|
|
||||||
$('#restartModal').modal({
|
|
||||||
backdrop: 'static',
|
|
||||||
show: true
|
|
||||||
});
|
|
||||||
} else if (settings_changed) {
|
|
||||||
sendConfig('settingsMenu');
|
|
||||||
} else {
|
|
||||||
$('#settingsMenu').modal('hide');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs4-jf.css
Normal file
7
jellyfin_accounts/data/static/bs4-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs4.css
Normal file
7
jellyfin_accounts/data/static/bs4.css
Normal file
File diff suppressed because one or more lines are too long
9438
jellyfin_accounts/data/static/bs5-jf.css
Normal file
9438
jellyfin_accounts/data/static/bs5-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs5.css
Normal file
7
jellyfin_accounts/data/static/bs5.css
Normal file
File diff suppressed because one or more lines are too long
25
jellyfin_accounts/data/static/serialize.js
Normal file
25
jellyfin_accounts/data/static/serialize.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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 'select-one':
|
||||||
|
case 'email':
|
||||||
|
formData[name] = el.value;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return formData;
|
||||||
|
};
|
@ -1,35 +1,41 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<meta name="msapplication-TileColor" content="#603cba">
|
<meta name="msapplication-TileColor" content="#603cba">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<title>404</title>
|
<title>404</title>
|
||||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
{% if not bs5 %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
{% endif %}
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||||
<style>
|
{% if bs5 %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<style>
|
||||||
.messageBox {
|
.messageBox {
|
||||||
margin: 20%;
|
margin: 20%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="messageBox">
|
<div class="messageBox">
|
||||||
<h1>Page not found.</h1>
|
<h1>Page not found.</h1>
|
||||||
<p>
|
<p>
|
||||||
{{ contactMessage }}
|
{{ contactMessage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -12,14 +12,18 @@
|
|||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<meta name="msapplication-TileColor" content="#603cba">
|
<meta name="msapplication-TileColor" content="#603cba">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
|
||||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></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>
|
||||||
|
{% endif %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
|
{% if bs5 %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||||
|
{% endif %}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<style>
|
<style>
|
||||||
.pageContainer {
|
.pageContainer {
|
||||||
@ -49,17 +53,11 @@
|
|||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
.fa-clipboard {
|
|
||||||
color: grey;
|
|
||||||
}
|
|
||||||
.fa-clipboard:hover {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<title>Admin</title>
|
<title>Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="modal fade" id="login" tabindex="-1" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
<div class="modal fade" id="login" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -82,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="settingsMenu" tabindex="-1" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
|
<div class="modal fade" id="settingsMenu" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -111,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="users" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
|
<div class="modal fade" id="users" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -130,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="userDefaults" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
|
<div class="modal fade" id="userDefaults" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -153,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="restartModal" tabindex="-1" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
|
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -176,13 +174,13 @@
|
|||||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
<button type="button" class="btn btn-secondary" id="openSettings">
|
||||||
Settings <i class="fa fa-cog"></i>
|
Settings <i class="fa fa-cog"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="card bg-light mb-3 linkGroup">
|
<div class="card mb-3 linkGroup">
|
||||||
<div class="card-header">Current Invites</div>
|
<div class="card-header">Current Invites</div>
|
||||||
<ul class="list-group list-group-flush" id="invites">
|
<ul class="list-group list-group-flush" id="invites">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="linkForm">
|
<div class="linkForm">
|
||||||
<div class="card bg-light mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">Generate Invite</div>
|
<div class="card-header">Generate Invite</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="#" method="POST" id="inviteForm">
|
<form action="#" method="POST" id="inviteForm">
|
||||||
@ -200,16 +198,14 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="send_to_address">Send invite to address</label>
|
<label for="send_to_address">Send invite to address</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
<input type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" id="generateSubmit" class="btn btn-primary">Generate</button>
|
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -218,6 +214,7 @@
|
|||||||
<p>{{ contactMessage }}</p>
|
<p>{{ contactMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="serialize.js"></script>
|
||||||
<script src="admin.js"></script>
|
<script src="admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
745
jellyfin_accounts/data/templates/admin.js
Normal file
745
jellyfin_accounts/data/templates/admin.js
Normal file
@ -0,0 +1,745 @@
|
|||||||
|
var bsVersion = {{ bsVersion }};
|
||||||
|
|
||||||
|
if (bsVersion == 5) {
|
||||||
|
function createModal(id, find = false) {
|
||||||
|
if (find) {
|
||||||
|
return bootstrap.Modal.getInstance(document.getElementById(id));
|
||||||
|
};
|
||||||
|
return new bootstrap.Modal(document.getElementById(id));
|
||||||
|
};
|
||||||
|
function triggerTooltips() {
|
||||||
|
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
|
||||||
|
// Hack to ensure anything dependent on checkboxes are disabled if necessary
|
||||||
|
var checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].click();
|
||||||
|
checkboxes[i].click();
|
||||||
|
};
|
||||||
|
// Initialize tooltips
|
||||||
|
var to_trigger = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||||
|
var tooltips = to_trigger.map(function(el) {
|
||||||
|
return new bootstrap.Tooltip(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// var loginModal = new bootstrap.Modal(document.getElementById('login'));
|
||||||
|
// var settingsModal = new bootstrap.Modal(document.getElementById('settingsMenu'));
|
||||||
|
// var userDefaultsModal = new bootstrap.Modal(document.getElementById('userDefaults'));
|
||||||
|
// var usersModal = new bootstrap.Modal(document.getElementById('users'));
|
||||||
|
// var restartModal = new bootstrap.Modal(document.getElementById('restartModal'));
|
||||||
|
} else if (bsVersion == 4) {
|
||||||
|
document.getElementById('send_to_address_enabled').classList.remove('form-check-input');
|
||||||
|
function createModal(id, find = false) {
|
||||||
|
return {
|
||||||
|
show : function() {
|
||||||
|
return $('#' + id).modal('show');
|
||||||
|
},
|
||||||
|
hide : function() {
|
||||||
|
return $('#' + id).modal('hide');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
function triggerTooltips() {
|
||||||
|
$('#settingsMenu').on('shown.bs.modal', function() {
|
||||||
|
var checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].click();
|
||||||
|
checkboxes[i].click();
|
||||||
|
};
|
||||||
|
$("a[data-toggle='tooltip']").each(function (i, obj) {
|
||||||
|
$(obj).tooltip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var loginModal = createModal('login');
|
||||||
|
var settingsModal = createModal('settingsMenu');
|
||||||
|
var userDefaultsModal = createModal('userDefaults');
|
||||||
|
var usersModal = createModal('users');
|
||||||
|
var restartModal = createModal('restartModal');
|
||||||
|
|
||||||
|
function parseInvite(invite, empty = false) {
|
||||||
|
if (empty === true) {
|
||||||
|
return ["None", "", "1"]
|
||||||
|
} else {
|
||||||
|
var i = ["", "", "0", invite['email']];
|
||||||
|
i[0] = invite['code'];
|
||||||
|
if (invite['hours'] == 0) {
|
||||||
|
i[1] = invite['minutes'] + 'm';
|
||||||
|
} else if (invite['minutes'] == 0) {
|
||||||
|
i[1] = invite['hours'] + 'h';
|
||||||
|
} else {
|
||||||
|
i[1] = invite['hours'] + 'h ' + invite['minutes'] + 'm';
|
||||||
|
}
|
||||||
|
i[1] = "Expires in " + i[1] + " ";
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addItem(invite) {
|
||||||
|
var links = document.getElementById('invites');
|
||||||
|
var listItem = document.createElement('li');
|
||||||
|
listItem.id = invite[0]
|
||||||
|
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||||
|
var listCode = document.createElement('div');
|
||||||
|
listCode.classList.add('d-flex', 'align-items-center', 'font-monospace');
|
||||||
|
var codeLink = document.createElement('a');
|
||||||
|
codeLink.setAttribute('style', 'margin-right: 0.5rem;');
|
||||||
|
codeLink.appendChild(document.createTextNode(invite[0].replace(/-/g, '‑')));
|
||||||
|
listCode.appendChild(codeLink);
|
||||||
|
listItem.appendChild(listCode);
|
||||||
|
var listRight = document.createElement('div');
|
||||||
|
listText = document.createElement('span');
|
||||||
|
listText.id = invite[0] + '_expiry'
|
||||||
|
listText.appendChild(document.createTextNode(invite[1]));
|
||||||
|
listRight.appendChild(listText);
|
||||||
|
if (invite[2] == 0) {
|
||||||
|
var inviteCode = window.location.href.replace('#', '') + 'invite/' + invite[0];
|
||||||
|
codeLink.href = inviteCode;
|
||||||
|
// listCode.appendChild(document.createTextNode(" "));
|
||||||
|
var codeCopy = document.createElement('i');
|
||||||
|
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
||||||
|
codeCopy.classList.add('fa', 'fa-clipboard', 'icon-button');
|
||||||
|
listCode.appendChild(codeCopy);
|
||||||
|
if (typeof(invite[3]) != 'undefined') {
|
||||||
|
var sentTo = document.createElement('span');
|
||||||
|
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
|
||||||
|
if (invite[3].includes('Failed to send to')) {
|
||||||
|
sentTo.appendChild(document.createTextNode(invite[3]));
|
||||||
|
} else {
|
||||||
|
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
|
||||||
|
}
|
||||||
|
listCode.appendChild(sentTo);
|
||||||
|
};
|
||||||
|
var listDelete = document.createElement('button');
|
||||||
|
listDelete.onclick = function(){deleteInvite(invite[0])};
|
||||||
|
listDelete.classList.add('btn', 'btn-outline-danger');
|
||||||
|
listDelete.appendChild(document.createTextNode('Delete'));
|
||||||
|
listRight.appendChild(listDelete);
|
||||||
|
};
|
||||||
|
listItem.appendChild(listRight);
|
||||||
|
links.appendChild(listItem);
|
||||||
|
};
|
||||||
|
function updateInvite(invite) {
|
||||||
|
var expiry = document.getElementById(invite[0] + '_expiry');
|
||||||
|
expiry.textContent = invite[1];
|
||||||
|
}
|
||||||
|
function removeInvite(code) {
|
||||||
|
var item = document.getElementById(code);
|
||||||
|
item.parentNode.removeChild(item);
|
||||||
|
}
|
||||||
|
function generateInvites(empty = false) {
|
||||||
|
// document.getElementById('invites').textContent = '';
|
||||||
|
if (empty === false) {
|
||||||
|
var 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;
|
||||||
|
if (data['invites'].length == 0) {
|
||||||
|
document.getElementById('invites').textContent = '';
|
||||||
|
addItem(parseInvite([], true));
|
||||||
|
} else {
|
||||||
|
data['invites'].forEach(function(invite) {
|
||||||
|
var match = false;
|
||||||
|
var items = document.getElementById('invites').children;
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].id == invite['code']) {
|
||||||
|
match = true;
|
||||||
|
updateInvite(parseInvite(invite));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (match == false) {
|
||||||
|
addItem(parseInvite(invite));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var items = document.getElementById('invites').children;
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var exists = false;
|
||||||
|
data['invites'].forEach(function(invite) {
|
||||||
|
if (items[i].id == invite['code']) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (exists == false) {
|
||||||
|
removeInvite(items[i].id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
} else if (empty === true) {
|
||||||
|
document.getElementById('invites').textContent = '';
|
||||||
|
addItem(parseInvite([], true));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
function deleteInvite(code) {
|
||||||
|
var send = JSON.stringify({ "code": code });
|
||||||
|
var 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);
|
||||||
|
};
|
||||||
|
function addOptions(le, sel) {
|
||||||
|
for (v = 0; v <= le; v++) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.appendChild(document.createTextNode(v));
|
||||||
|
opt.value = v;
|
||||||
|
sel.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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('inviteForm').onsubmit = function() {
|
||||||
|
var button = document.getElementById('generateSubmit');
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
send_object = serializeForm('inviteForm');
|
||||||
|
console.log(send_object);
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var send = JSON.stringify(send_object);
|
||||||
|
var 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;
|
||||||
|
};
|
||||||
|
document.getElementById('loginForm').onsubmit = function() {
|
||||||
|
window.token = "";
|
||||||
|
var details = serializeForm('loginForm');
|
||||||
|
var errorArea = document.getElementById('loginErrorArea');
|
||||||
|
errorArea.textContent = '';
|
||||||
|
var button = document.getElementById('loginSubmit');
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 401) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Login';
|
||||||
|
var wrongPassword = document.createElement('div');
|
||||||
|
wrongPassword.classList.add('alert', 'alert-danger');
|
||||||
|
wrongPassword.setAttribute('role', 'alert');
|
||||||
|
wrongPassword.appendChild(document.createTextNode('Incorrect username or password.'));
|
||||||
|
errorArea.appendChild(wrongPassword);
|
||||||
|
} else {
|
||||||
|
var data = this.response;
|
||||||
|
window.token = data['token'];
|
||||||
|
generateInvites();
|
||||||
|
var interval = setInterval(function() { generateInvites(); }, 60 * 1000);
|
||||||
|
var hour = document.getElementById('hours');
|
||||||
|
addOptions(24, hour);
|
||||||
|
hour.selected = "0";
|
||||||
|
var minutes = document.getElementById('minutes');
|
||||||
|
addOptions(59, minutes);
|
||||||
|
minutes.selected = "30";
|
||||||
|
loginModal.hide();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
req.open("GET", "/getToken", true);
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
||||||
|
req.send();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
document.getElementById('openDefaultsWizard').onclick = function() {
|
||||||
|
this.disabled = true
|
||||||
|
this.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
var 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) {
|
||||||
|
var users = req.response['users'];
|
||||||
|
var radioList = document.getElementById('defaultUserRadios');
|
||||||
|
radioList.textContent = '';
|
||||||
|
if (document.getElementById('setDefaultUser')) {
|
||||||
|
document.getElementById('setDefaultUser').remove();
|
||||||
|
};
|
||||||
|
for (var i = 0; i < users.length; i++) {
|
||||||
|
var user = users[i]
|
||||||
|
var radio = document.createElement('div');
|
||||||
|
radio.classList.add('radio');
|
||||||
|
if (i == 0) {
|
||||||
|
var checked = 'checked';
|
||||||
|
} else {
|
||||||
|
var checked = '';
|
||||||
|
};
|
||||||
|
radio.innerHTML =
|
||||||
|
'<label><input type="radio" name="defaultRadios" id="default_' +
|
||||||
|
user['name'] + '" style="margin-right: 1rem;"' + checked + '>' +
|
||||||
|
user['name'] + '</label>';
|
||||||
|
radioList.appendChild(radio);
|
||||||
|
}
|
||||||
|
var button = document.getElementById('openDefaultsWizard');
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = 'Set new account defaults';
|
||||||
|
var 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();
|
||||||
|
userDefaultsModal.show();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
};
|
||||||
|
document.getElementById('storeDefaults').onclick = function () {
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
var button = document.getElementById('storeDefaults');
|
||||||
|
var radios = document.getElementsByName('defaultRadios');
|
||||||
|
for (var i = 0; i < radios.length; i++) {
|
||||||
|
if (radios[i].checked) {
|
||||||
|
var data = {'username':radios[i].id.slice(8), 'homescreen':false};
|
||||||
|
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||||
|
data['homescreen'] = true;
|
||||||
|
}
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("POST", "/setDefaults", 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(){$('#userDefaults').modal('hide');}, 1000);
|
||||||
|
} else {
|
||||||
|
button.textContent = 'Failed';
|
||||||
|
button.classList.remove('btn-primary');
|
||||||
|
button.classList.add('btn-danger');
|
||||||
|
setTimeout(function(){
|
||||||
|
var 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));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// $.ajax('/setDefaults', {
|
||||||
|
// data : JSON.stringify(data),
|
||||||
|
// contentType : 'application/json',
|
||||||
|
// type : 'POST',
|
||||||
|
// xhrFields : {
|
||||||
|
// withCredentials: true
|
||||||
|
// },
|
||||||
|
// beforeSend : function (xhr) {
|
||||||
|
// xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
// },
|
||||||
|
// success: function() {
|
||||||
|
// 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(){$('#userDefaults').modal('hide');}, 1000);
|
||||||
|
// },
|
||||||
|
// error: function() {
|
||||||
|
// button.textContent = 'Failed';
|
||||||
|
// button.classList.remove('btn-primary');
|
||||||
|
// button.classList.add('btn-danger');
|
||||||
|
// setTimeout(function(){
|
||||||
|
// var button = document.getElementById('storeDefaults');
|
||||||
|
// button.textContent = 'Submit';
|
||||||
|
// button.classList.remove('btn-danger');
|
||||||
|
// button.classList.add('btn-primary');
|
||||||
|
// button.disabled = false;
|
||||||
|
// }, 1000);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
document.getElementById('openUsers').onclick = function () {
|
||||||
|
this.disabled = true;
|
||||||
|
this.innerHTML =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'Loading...';
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("GET", "/getUsers", true);
|
||||||
|
req.responseType = 'json';
|
||||||
|
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||||
|
req.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
var list = document.getElementById('userList');
|
||||||
|
list.textContent = '';
|
||||||
|
if (document.getElementById('saveUsers')) {
|
||||||
|
document.getElementById('saveUsers').remove();
|
||||||
|
};
|
||||||
|
var users = req.response['users'];
|
||||||
|
for (var i = 0; i < users.length; i++) {
|
||||||
|
var user = users[i]
|
||||||
|
var entry = document.createElement('div');
|
||||||
|
entry.classList.add('form-group', 'list-group-item', 'py-1');
|
||||||
|
entry.id = 'user_' + user['name'];
|
||||||
|
var label = document.createElement('label');
|
||||||
|
label.classList.add('d-inline-block');
|
||||||
|
label.setAttribute('for', 'address_' + user['email']);
|
||||||
|
label.appendChild(document.createTextNode(user['name']));
|
||||||
|
entry.appendChild(label);
|
||||||
|
var address = document.createElement('input');
|
||||||
|
address.setAttribute('type', 'email');
|
||||||
|
address.readOnly = true;
|
||||||
|
address.classList.add('form-control-plaintext', 'text-muted', 'd-inline-block');
|
||||||
|
//address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
|
||||||
|
address.classList.add('addressText');
|
||||||
|
address.id = 'address_' + user['email'];
|
||||||
|
if (typeof(user['email']) != 'undefined') {
|
||||||
|
address.value = user['email'];
|
||||||
|
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||||
|
};
|
||||||
|
var editButton = document.createElement('i');
|
||||||
|
editButton.classList.add('fa', 'fa-edit', 'd-inline-block', 'icon-button');
|
||||||
|
editButton.setAttribute('style', 'margin-left: 2%;');
|
||||||
|
editButton.onclick = function() {
|
||||||
|
this.classList.remove('fa', 'fa-edit');
|
||||||
|
// var input = document.createElement('input');
|
||||||
|
// input.setAttribute('type', 'email');
|
||||||
|
// input.classList.add('email-input');
|
||||||
|
//var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
|
||||||
|
var addressElement = this.parentNode.getElementsByClassName('form-control-plaintext')[0];
|
||||||
|
addressElement.classList.remove('form-control-plaintext', 'text-muted');
|
||||||
|
addressElement.classList.add('form-control');
|
||||||
|
addressElement.readOnly = false;
|
||||||
|
if (addressElement.value == '') {
|
||||||
|
// input.value = addressElement.textContent;
|
||||||
|
// } else {
|
||||||
|
addressElement.placeholder = 'Email Address';
|
||||||
|
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||||
|
};
|
||||||
|
// this.parentNode.replaceChild(input, addressElement);
|
||||||
|
if (document.getElementById('saveUsers') == null) {
|
||||||
|
var footer = document.getElementById('userFooter')
|
||||||
|
var saveUsers = document.createElement('input');
|
||||||
|
saveUsers.classList.add('btn', 'btn-primary');
|
||||||
|
saveUsers.setAttribute('type', 'button');
|
||||||
|
saveUsers.value = 'Save Changes';
|
||||||
|
saveUsers.id = 'saveUsers';
|
||||||
|
saveUsers.onclick = function() {
|
||||||
|
var send = {}
|
||||||
|
var entries = document.getElementById('userList').children;
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i];
|
||||||
|
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
||||||
|
var name = entry.id.replace(/user_/g, '')
|
||||||
|
var address = entry.getElementsByTagName('input')[0].value;
|
||||||
|
send[name] = address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
send = JSON.stringify(send);
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("POST", "/modifyUsers", 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) {
|
||||||
|
usersModal.hide();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
req.send(send);
|
||||||
|
};
|
||||||
|
footer.appendChild(saveUsers);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
entry.appendChild(editButton);
|
||||||
|
entry.appendChild(address);
|
||||||
|
list.appendChild(entry);
|
||||||
|
};
|
||||||
|
var button = document.getElementById('openUsers');
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
||||||
|
settingsModal.hide();
|
||||||
|
usersModal.show();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
generateInvites(empty = true);
|
||||||
|
loginModal.show()
|
||||||
|
|
||||||
|
var config = {};
|
||||||
|
var modifiedConfig = {};
|
||||||
|
|
||||||
|
document.getElementById('openSettings').onclick = function () {
|
||||||
|
restart_setting_changed = false;
|
||||||
|
var 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) {
|
||||||
|
var settingsList = document.getElementById('settingsList');
|
||||||
|
settingsList.textContent = '';
|
||||||
|
config = this.response;
|
||||||
|
for (var section of Object.keys(config)) {
|
||||||
|
var sectionCollapse = document.createElement('div');
|
||||||
|
sectionCollapse.classList.add('collapse');
|
||||||
|
sectionCollapse.id = section;
|
||||||
|
|
||||||
|
var sectionTitle = config[section]['meta']['name'];
|
||||||
|
var sectionDescription = config[section]['meta']['description'];
|
||||||
|
var entryListID = section + '_entryList';
|
||||||
|
var sectionFooter = section + '_footer';
|
||||||
|
|
||||||
|
var innerCollapse = `
|
||||||
|
<div class="card card-body">
|
||||||
|
<small class="text-muted">${sectionDescription}</small>
|
||||||
|
<div class="${entryListID}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
sectionCollapse.innerHTML = innerCollapse;
|
||||||
|
|
||||||
|
for (var entry of Object.keys(config[section])) {
|
||||||
|
if (entry != 'meta') {
|
||||||
|
var entryName = config[section][entry]['name'];
|
||||||
|
var 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 (config[section][entry].hasOwnProperty('description')) {
|
||||||
|
var tooltip = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
entryName += ' ';
|
||||||
|
entryName += tooltip;
|
||||||
|
};
|
||||||
|
var entryValue = config[section][entry]['value'];
|
||||||
|
var entryType = config[section][entry]['type'];
|
||||||
|
var 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 = `
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}">
|
||||||
|
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<label for="${section}_${entry}">${entryName}</label>
|
||||||
|
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}">
|
||||||
|
`;
|
||||||
|
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||||
|
} else if (entryType == 'select') {
|
||||||
|
entryGroup.classList.add('form-group');
|
||||||
|
var entryOptions = config[section][entry]['options'];
|
||||||
|
var innerGroup = `
|
||||||
|
<label for="${section}_${entry}">${entryName}</label>
|
||||||
|
<select class="form-control" id="${section}_${entry}">
|
||||||
|
`;
|
||||||
|
for (var i = 0; i < entryOptions.length; i++) {
|
||||||
|
if (entryOptions[i] == entryValue) {
|
||||||
|
var selected = 'selected';
|
||||||
|
} else {
|
||||||
|
var selected = '';
|
||||||
|
}
|
||||||
|
innerGroup += `
|
||||||
|
<option value="${entryOptions[i]}" ${selected}>${entryOptions[i]}</option>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
innerGroup += '</select>';
|
||||||
|
entryGroup.innerHTML = innerGroup;
|
||||||
|
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||||
|
|
||||||
|
};
|
||||||
|
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var 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();
|
||||||
|
//
|
||||||
|
// $('#settingsMenu').on('shown.bs.modal', function() {
|
||||||
|
// $("a[data-toggle='tooltip']").each(function (i, obj) {
|
||||||
|
// $(obj).tooltip();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
function sendConfig(modalId) {
|
||||||
|
var modal = document.getElementById(modalId);
|
||||||
|
var send = JSON.stringify(modifiedConfig);
|
||||||
|
var 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();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// fail: function(xhr, textStatus, errorThrown) {
|
||||||
|
// var footer = modal.getElementsByClassName('modal-dialog')[0].getElementsByClassName('modal-content')[0].getElementsByClassName('modal-footer')[0];
|
||||||
|
// var alert = document.createElement('div');
|
||||||
|
// alert.classList.add('alert', 'alert-danger');
|
||||||
|
// alert.setAttribute('role', 'alert');
|
||||||
|
// alert.appendChild(document.createTextNode('Error: ' + errorThrown));
|
||||||
|
// footer.appendChild(alert);
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
req.send(send);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('settingsSave').onclick = function() {
|
||||||
|
modifiedConfig = {};
|
||||||
|
var restart_setting_changed = false;
|
||||||
|
var settings_changed = false;
|
||||||
|
|
||||||
|
for (var section of Object.keys(config)) {
|
||||||
|
for (var entry of Object.keys(config[section])) {
|
||||||
|
if (entry != 'meta') {
|
||||||
|
var entryID = section + '_' + entry;
|
||||||
|
var 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) {
|
||||||
|
if (restart_setting_changed) {
|
||||||
|
document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
|
||||||
|
settingsModal.hide();
|
||||||
|
restartModal.show();
|
||||||
|
} else if (settings_changed) {
|
||||||
|
sendConfig('settingsMenu');
|
||||||
|
} else {
|
||||||
|
settingsModal.hide();
|
||||||
|
};
|
||||||
|
};
|
@ -12,12 +12,18 @@
|
|||||||
<meta name="msapplication-TileColor" content="#603cba">
|
<meta name="msapplication-TileColor" content="#603cba">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
{% if not bs5 %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
{% endif %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||||
|
{% if bs5 %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<style>
|
<style>
|
||||||
.pageContainer {
|
.pageContainer {
|
||||||
margin: 5% 20% 5% 20%;
|
margin: 5% 20% 5% 20%;
|
||||||
@ -61,11 +67,11 @@
|
|||||||
<p class="contactBox">{{ contactMessage }}</p>
|
<p class="contactBox">{{ contactMessage }}</p>
|
||||||
<div class="container" id="container">
|
<div class="container" id="container">
|
||||||
<div class="row" id="cardContainer">
|
<div class="row" id="cardContainer">
|
||||||
<div class="col-sm" id="accountForm">
|
<div class="col-sm">
|
||||||
<div class="card bg-light mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">Details</div>
|
<div class="card-header">Details</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="#" method="POST">
|
<form action="#" method="POST" id="accountForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputEmail">Email</label>
|
<label for="inputEmail">Email</label>
|
||||||
<input type="email" class="form-control" id="{% if username %}inputEmail{% else %}inputUsername{% endif %}" name="{% if username %}email{% else %}username{% endif %}" placeholder="Email" value="{{ email }}" required>
|
<input type="email" class="form-control" id="{% if username %}inputEmail{% else %}inputUsername{% endif %}" name="{% if username %}email{% else %}username{% endif %}" placeholder="Email" value="{{ email }}" required>
|
||||||
@ -80,7 +86,7 @@
|
|||||||
<label for="inputPassword">Password</label>
|
<label for="inputPassword">Password</label>
|
||||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox">
|
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;">
|
||||||
<button type="submit" class="btn btn-outline-primary" id="submitButton">
|
<button type="submit" class="btn btn-outline-primary" id="submitButton">
|
||||||
<span id="createAccount">Create Account</span>
|
<span id="createAccount">Create Account</span>
|
||||||
</button>
|
</button>
|
||||||
@ -91,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if validate %}
|
{% if validate %}
|
||||||
<div class="col-sm" id="requirementBox">
|
<div class="col-sm" id="requirementBox">
|
||||||
<div class="card bg-light mb-3 requirementBox">
|
<div class="card mb-3 requirementBox">
|
||||||
<div class="card-header">Password Requirements</div>
|
<div class="card-header">Password Requirements</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@ -108,7 +114,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="serialize.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
{% if bs5 %}
|
||||||
|
var bsVersion = 5;
|
||||||
|
{% else %}
|
||||||
|
var bsVersion = 4;
|
||||||
|
{% endif %}
|
||||||
|
if (bsVersion == 5) {
|
||||||
|
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
|
||||||
|
} else if (bsVersion == 4) {
|
||||||
|
var successBox = {
|
||||||
|
show : function() {
|
||||||
|
return $('#successBox').modal('show');
|
||||||
|
},
|
||||||
|
hide : function() {
|
||||||
|
return $('#successBox').modal('hide');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
var code = window.location.href.split('/').pop();
|
var code = window.location.href.split('/').pop();
|
||||||
function toggleSpinner () {
|
function toggleSpinner () {
|
||||||
var submitButton = document.getElementById('submitButton');
|
var submitButton = document.getElementById('submitButton');
|
||||||
@ -131,25 +155,24 @@
|
|||||||
}
|
}
|
||||||
submitButton.replaceChild(newSpan, oldSpan);
|
submitButton.replaceChild(newSpan, oldSpan);
|
||||||
};
|
};
|
||||||
$("form").submit(function() {
|
document.getElementById('accountForm').onsubmit = function() {
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var send = $("form").serializeObject();
|
var send = serializeForm('accountForm');
|
||||||
send['code'] = code;
|
send['code'] = code;
|
||||||
{% if not username %}
|
{% if not username %}
|
||||||
send['email'] = send['username'];
|
send['email'] = send['username'];
|
||||||
{% endif %}
|
{% endif %}
|
||||||
send = JSON.stringify(send);
|
send = JSON.stringify(send);
|
||||||
$.ajax('/newUser', {
|
var req = new XMLHttpRequest();
|
||||||
data : send,
|
req.open("POST", "/newUser", true);
|
||||||
contentType : 'application/json',
|
req.responseType = 'json';
|
||||||
type : 'POST',
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
crossDomain : true,
|
req.onreadystatechange = function() {
|
||||||
complete : function(response){
|
if (this.readyState == 4) {
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var data = response['responseJSON'];
|
var data = this.response;
|
||||||
if ('error' in data) {
|
if ('error' in data) {
|
||||||
var text = document.createTextNode(data['error']);
|
var text = document.createTextNode(data['error']);
|
||||||
// <div class="alert alert-danger" id="errorBox"></div>
|
|
||||||
var error = document.createElement('button');
|
var error = document.createElement('button');
|
||||||
error.classList.add('btn', 'btn-outline-danger');
|
error.classList.add('btn', 'btn-outline-danger');
|
||||||
error.setAttribute('disabled', '');
|
error.setAttribute('disabled', '');
|
||||||
@ -177,13 +200,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (valid == true) {
|
if (valid == true) {
|
||||||
$('#successBox').modal('show');
|
successBox.show();
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
});
|
};
|
||||||
|
req.send(send);
|
||||||
return false;
|
return false;
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,25 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>Invalid Code</title>
|
<title>Invalid Code</title>
|
||||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
{% if not bs5 %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
{% endif %}
|
||||||
<style>
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||||
|
{% if bs5 %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<style>
|
||||||
.messageBox {
|
.messageBox {
|
||||||
margin: 20%;
|
margin: 20%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="messageBox">
|
<div class="messageBox">
|
||||||
<h1>Invalid Code.</h1>
|
<h1>Invalid Code.</h1>
|
||||||
<p>The above code is either incorrect, or has expired.</p>
|
<p>The above code is either incorrect, or has expired.</p>
|
||||||
<p>{{ contactMessage }}</p>
|
<p>{{ contactMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, send_from_directory, render_template
|
from flask import Flask, send_from_directory, render_template
|
||||||
|
|
||||||
from jellyfin_accounts import app, g, css, data_store
|
from jellyfin_accounts import app, g, css_file, data_store
|
||||||
from jellyfin_accounts import web_log as log
|
from jellyfin_accounts import web_log as log
|
||||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
from jellyfin_accounts.web_api import config, checkInvite, validator
|
||||||
|
|
||||||
@ -11,9 +11,8 @@ def page_not_found(e):
|
|||||||
return (
|
return (
|
||||||
render_template(
|
render_template(
|
||||||
"404.html",
|
"404.html",
|
||||||
css_href=css["href"],
|
bs5=config.getboolean("ui", "bs5"),
|
||||||
css_integrity=css["integrity"],
|
css_file=css_file,
|
||||||
css_crossorigin=css["crossorigin"],
|
|
||||||
contactMessage=config["ui"]["contact_message"],
|
contactMessage=config["ui"]["contact_message"],
|
||||||
),
|
),
|
||||||
404,
|
404,
|
||||||
@ -25,9 +24,8 @@ def admin():
|
|||||||
# return app.send_static_file('admin.html')
|
# return app.send_static_file('admin.html')
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin.html",
|
"admin.html",
|
||||||
css_href=css["href"],
|
bs5=config.getboolean("ui", "bs5"),
|
||||||
css_integrity=css["integrity"],
|
css_file=css_file,
|
||||||
css_crossorigin=css["crossorigin"],
|
|
||||||
contactMessage="",
|
contactMessage="",
|
||||||
email_enabled=config.getboolean("invite_emails", "enabled"),
|
email_enabled=config.getboolean("invite_emails", "enabled"),
|
||||||
)
|
)
|
||||||
@ -36,13 +34,18 @@ def admin():
|
|||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def static_proxy(path):
|
def static_proxy(path):
|
||||||
if "html" not in path:
|
if "html" not in path:
|
||||||
|
if "admin.js" in path:
|
||||||
|
if config.getboolean("ui", "bs5"):
|
||||||
|
bsVersion = 5
|
||||||
|
else:
|
||||||
|
bsVersion = 4
|
||||||
|
return render_template("admin.js", bsVersion=bsVersion)
|
||||||
return app.send_static_file(path)
|
return app.send_static_file(path)
|
||||||
return (
|
return (
|
||||||
render_template(
|
render_template(
|
||||||
"404.html",
|
"404.html",
|
||||||
css_href=css["href"],
|
bs5=config.getboolean("ui", "bs5"),
|
||||||
css_integrity=css["integrity"],
|
css_file=css_file,
|
||||||
css_crossorigin=css["crossorigin"],
|
|
||||||
contactMessage=config["ui"]["contact_message"],
|
contactMessage=config["ui"]["contact_message"],
|
||||||
),
|
),
|
||||||
404,
|
404,
|
||||||
@ -59,9 +62,8 @@ def inviteProxy(path):
|
|||||||
email = ""
|
email = ""
|
||||||
return render_template(
|
return render_template(
|
||||||
"form.html",
|
"form.html",
|
||||||
css_href=css["href"],
|
bs5=config.getboolean("ui", "bs5"),
|
||||||
css_integrity=css["integrity"],
|
css_file=css_file,
|
||||||
css_crossorigin=css["crossorigin"],
|
|
||||||
contactMessage=config["ui"]["contact_message"],
|
contactMessage=config["ui"]["contact_message"],
|
||||||
helpMessage=config["ui"]["help_message"],
|
helpMessage=config["ui"]["help_message"],
|
||||||
successMessage=config["ui"]["success_message"],
|
successMessage=config["ui"]["success_message"],
|
||||||
@ -77,8 +79,7 @@ def inviteProxy(path):
|
|||||||
log.debug("Attempted use of invalid invite")
|
log.debug("Attempted use of invalid invite")
|
||||||
return render_template(
|
return render_template(
|
||||||
"invalidCode.html",
|
"invalidCode.html",
|
||||||
css_href=css["href"],
|
bs5=config.getboolean("ui", "bs5"),
|
||||||
css_integrity=css["integrity"],
|
css_file=css_file,
|
||||||
css_crossorigin=css["crossorigin"],
|
|
||||||
contactMessage=config["ui"]["contact_message"],
|
contactMessage=config["ui"]["contact_message"],
|
||||||
)
|
)
|
||||||
|
@ -338,13 +338,9 @@ def modifyConfig():
|
|||||||
for section in data:
|
for section in data:
|
||||||
if section in temp_config:
|
if section in temp_config:
|
||||||
for item in data[section]:
|
for item in data[section]:
|
||||||
if item in temp_config[section]:
|
|
||||||
temp_config[section][item] = data[section][item]
|
temp_config[section][item] = data[section][item]
|
||||||
data[section][item] = True
|
data[section][item] = True
|
||||||
log.debug(f"{section}/{item} modified")
|
log.debug(f"{section}/{item} modified")
|
||||||
else:
|
|
||||||
data[section][item] = False
|
|
||||||
log.debug(f"{section}/{item} does not exist in config")
|
|
||||||
with open(config_path, "w") as config_file:
|
with open(config_path, "w") as config_file:
|
||||||
temp_config.write(config_file)
|
temp_config.write(config_file)
|
||||||
config = load_config(config_path, data_dir)
|
config = load_config(config_path, data_dir)
|
||||||
|
8
jf-accounts.service
Normal file
8
jf-accounts.service
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=A basic account management system for Jellyfin.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/home/hrfee/.cache/pypoetry/virtualenvs/jellyfin-accounts-r2jcKHws-py3.8/bin/jf-accounts
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
2569
package-lock.json
generated
Normal file
2569
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jellyfin-accounts"
|
name = "jellyfin-accounts"
|
||||||
version = "0.2.6"
|
version = "0.3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "A simple account management system for Jellyfin"
|
description = "A simple account management system for Jellyfin"
|
||||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||||
@ -9,7 +9,7 @@ homepage = "https://github.com/hrfee/jellyfin-accounts"
|
|||||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||||
keywords = ["jellyfin", "jf-accounts"]
|
keywords = ["jellyfin", "jf-accounts"]
|
||||||
include = ["jellyfin_accounts/data/*"]
|
include = ["jellyfin_accounts/data/*"]
|
||||||
exclude = ["images/*"]
|
exclude = ["images/*", "scss/*"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
|
16
scss/README.md
Normal file
16
scss/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
## SCSS
|
||||||
|
|
||||||
|
* `bs<4/5>-jf.scss` contains the source for the customizations to bootstrap. To customize the UI, you can make modifications to this file and then compile it.
|
||||||
|
|
||||||
|
**Note**: For BS5, it is assumed that bootstrap is installed in `../../node_modules/bootstrap` relative to itself.
|
||||||
|
For BS4, it assumes that bootstrap is installed in `../../node_modules/bootstrap4` relative to itself (`npm install bootstrap4@npm:bootstrap`).
|
||||||
|
* Compilation requires a sass compiler of your choice, and `postcss-cli`, `autoprefixer` + `clean-css-cli` from npm.
|
||||||
|
* If you're using `sassc`, run `./compile.sh bs<4/5>-jf.scss` in this directory. This will create a .css file, and minified .css file.
|
||||||
|
* For `node-sass`, replace the `sassc` line in `compile.sh` with
|
||||||
|
```
|
||||||
|
node-sass --output-style expanded --precision 6 $1 $css_file
|
||||||
|
```
|
||||||
|
and run as above.
|
||||||
|
* If you're building from source, copy the minified css to `<jf-accounts git directory>/jellyfin_accounts/data/static/bs<4/5>-jf.css`.
|
||||||
|
* If you're just customizing your install, set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||||
|
|
9548
scss/bs4/bs4-jf.css
Normal file
9548
scss/bs4/bs4-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
scss/bs4/bs4-jf.min.css
vendored
Normal file
7
scss/bs4/bs4-jf.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
scss/bs4/bs4-jf.min.css.map
Normal file
1
scss/bs4/bs4-jf.min.css.map
Normal file
File diff suppressed because one or more lines are too long
136
scss/bs4/bs4-jf.scss
Normal file
136
scss/bs4/bs4-jf.scss
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
$jf-blue: rgb(0, 164, 220);
|
||||||
|
$jf-blue-hover: rgba(0, 164, 220, 0.2);
|
||||||
|
$jf-blue-focus: rgb(12, 176, 232);
|
||||||
|
$jf-blue-light: #4bb3dd;
|
||||||
|
|
||||||
|
$jf-red: rgb(204, 0, 0);
|
||||||
|
$jf-red-light: #e12026;
|
||||||
|
$jf-yellower: #ffc107;
|
||||||
|
$jf-yellow: #e1b222;
|
||||||
|
$jf-orange: #ff870f;
|
||||||
|
$jf-green: #6fbd45;
|
||||||
|
$jf-green-dark: #008040;
|
||||||
|
|
||||||
|
|
||||||
|
$jf-black: #101010; // 16 16 16
|
||||||
|
$jf-gray-90: #202020; // 32 32 32
|
||||||
|
$jf-gray-80: #242424; // jf-card 36 36 36
|
||||||
|
$jf-gray-70: #292929; // jf-input 41 41 41
|
||||||
|
$jf-gray-60: #303030; // jf-button 48 48 48
|
||||||
|
$jf-gray-50: #383838; // jf-button-focus 56 56 56
|
||||||
|
$jf-text-bold: rgba(255, 255, 255, 0.87);
|
||||||
|
$jf-text-primary: rgba(255, 255, 255, 0.8);
|
||||||
|
$jf-text-secondary: rgb(153, 153, 153);
|
||||||
|
|
||||||
|
$primary: $jf-blue;
|
||||||
|
$secondary: $jf-gray-50;
|
||||||
|
$success: $jf-green-dark;
|
||||||
|
$danger: $jf-red-light;
|
||||||
|
$light: $jf-text-primary;
|
||||||
|
$dark: $jf-gray-90;
|
||||||
|
$info: $jf-yellow;
|
||||||
|
$warning: $jf-yellower;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$enable-gradients: false;
|
||||||
|
$enable-shadows: false;
|
||||||
|
|
||||||
|
$enable-rounded: false;
|
||||||
|
$body-bg: $jf-black;
|
||||||
|
$body-color: $jf-text-primary;
|
||||||
|
$border-color: $jf-gray-60;
|
||||||
|
$component-active-color: $jf-text-bold;
|
||||||
|
$component-active-bg: $jf-blue-focus;
|
||||||
|
$text-muted: $jf-text-secondary;
|
||||||
|
$link-color: $jf-blue-focus;
|
||||||
|
$btn-link-disabled-color: $jf-text-secondary;
|
||||||
|
$input-bg: $jf-gray-90;
|
||||||
|
$input-color: $jf-text-primary;
|
||||||
|
$input-focus-bg: $jf-gray-60;
|
||||||
|
$input-focus-border-color: $jf-blue-focus;
|
||||||
|
$input-disabled-bg: $jf-gray-70;
|
||||||
|
input:disabled {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
$input-border-color: $jf-gray-60;
|
||||||
|
$input-placeholder-color: $text-muted;
|
||||||
|
|
||||||
|
$form-check-input-bg: $jf-gray-60;
|
||||||
|
$form-check-input-border: $jf-gray-50;
|
||||||
|
$form-check-input-checked-color: $jf-blue-focus;
|
||||||
|
$form-check-input-checked-bg-color: $jf-blue-hover;
|
||||||
|
|
||||||
|
$input-group-addon-bg: $input-bg;
|
||||||
|
|
||||||
|
$form-select-disabled-color: $jf-text-secondary;
|
||||||
|
$form-select-disabled-bg: $input-disabled-bg;
|
||||||
|
$form-select-indicator-color: $jf-gray-50;
|
||||||
|
|
||||||
|
$card-bg: $jf-gray-80;
|
||||||
|
$card-border-color: null;
|
||||||
|
|
||||||
|
$tooltip-color: $jf-text-bold;
|
||||||
|
$tooltip-bg: $jf-gray-50;
|
||||||
|
|
||||||
|
$modal-content-bg: $jf-gray-80;
|
||||||
|
$modal-content-border-color: $jf-gray-50;
|
||||||
|
$modal-header-border-color: null;
|
||||||
|
$modal-footer-border-color: null;
|
||||||
|
|
||||||
|
$list-group-bg: $card-bg;
|
||||||
|
$list-group-border-color: $jf-gray-50;
|
||||||
|
$list-group-hover-bg: $jf-blue-hover;
|
||||||
|
$list-group-active-bg: $jf-blue-focus;
|
||||||
|
$list-group-action-color: $jf-text-primary;
|
||||||
|
$list-group-action-hover-color: $jf-text-bold;
|
||||||
|
$list-group-action-active-color: $jf-text-bold;
|
||||||
|
$list-group-action-active-bg: $jf-blue-focus;
|
||||||
|
|
||||||
|
// idk why but i had to put these above and below the import
|
||||||
|
.list-group-item-danger {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-success {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
||||||
|
|
||||||
|
.btn-primary, .btn-outline-primary:hover, .btn-outline-primary:active {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $jf-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover, .close:active {
|
||||||
|
color: $jf-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-bright {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-danger {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-success {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
|
10
scss/bs4/compile.sh
Executable file
10
scss/bs4/compile.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||||
|
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||||
|
sassc -t expanded -p 6 $1 $css_file
|
||||||
|
echo "Compiled."
|
||||||
|
postcss $css_file --replace --use autoprefixer
|
||||||
|
echo "Prefixed."
|
||||||
|
echo "Written to $css_file."
|
||||||
|
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||||
|
echo "Minified version written to $min_file."
|
9438
scss/bs5/bs5-jf.css
Normal file
9438
scss/bs5/bs5-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
scss/bs5/bs5-jf.min.css
vendored
Normal file
7
scss/bs5/bs5-jf.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
scss/bs5/bs5-jf.min.css.map
Normal file
1
scss/bs5/bs5-jf.min.css.map
Normal file
File diff suppressed because one or more lines are too long
136
scss/bs5/bs5-jf.scss
Normal file
136
scss/bs5/bs5-jf.scss
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
$jf-blue: rgb(0, 164, 220);
|
||||||
|
$jf-blue-hover: rgba(0, 164, 220, 0.2);
|
||||||
|
$jf-blue-focus: rgb(12, 176, 232);
|
||||||
|
$jf-blue-light: #4bb3dd;
|
||||||
|
|
||||||
|
$jf-red: rgb(204, 0, 0);
|
||||||
|
$jf-red-light: #e12026;
|
||||||
|
$jf-yellower: #ffc107;
|
||||||
|
$jf-yellow: #e1b222;
|
||||||
|
$jf-orange: #ff870f;
|
||||||
|
$jf-green: #6fbd45;
|
||||||
|
$jf-green-dark: #008040;
|
||||||
|
|
||||||
|
|
||||||
|
$jf-black: #101010; // 16 16 16
|
||||||
|
$jf-gray-90: #202020; // 32 32 32
|
||||||
|
$jf-gray-80: #242424; // jf-card 36 36 36
|
||||||
|
$jf-gray-70: #292929; // jf-input 41 41 41
|
||||||
|
$jf-gray-60: #303030; // jf-button 48 48 48
|
||||||
|
$jf-gray-50: #383838; // jf-button-focus 56 56 56
|
||||||
|
$jf-text-bold: rgba(255, 255, 255, 0.87);
|
||||||
|
$jf-text-primary: rgba(255, 255, 255, 0.8);
|
||||||
|
$jf-text-secondary: rgb(153, 153, 153);
|
||||||
|
|
||||||
|
$primary: $jf-blue;
|
||||||
|
$secondary: $jf-gray-50;
|
||||||
|
$success: $jf-green-dark;
|
||||||
|
$danger: $jf-red-light;
|
||||||
|
$light: $jf-text-primary;
|
||||||
|
$dark: $jf-gray-90;
|
||||||
|
$info: $jf-yellow;
|
||||||
|
$warning: $jf-yellower;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$enable-gradients: false;
|
||||||
|
$enable-shadows: false;
|
||||||
|
|
||||||
|
$enable-rounded: false;
|
||||||
|
$body-bg: $jf-black;
|
||||||
|
$body-color: $jf-text-primary;
|
||||||
|
$border-color: $jf-gray-60;
|
||||||
|
$component-active-color: $jf-text-bold;
|
||||||
|
$component-active-bg: $jf-blue-focus;
|
||||||
|
$text-muted: $jf-text-secondary;
|
||||||
|
$link-color: $jf-blue-focus;
|
||||||
|
$btn-link-disabled-color: $jf-text-secondary;
|
||||||
|
$input-bg: $jf-gray-90;
|
||||||
|
$input-color: $jf-text-primary;
|
||||||
|
$input-focus-bg: $jf-gray-60;
|
||||||
|
$input-focus-border-color: $jf-blue-focus;
|
||||||
|
$input-disabled-bg: $jf-gray-70;
|
||||||
|
input:disabled {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
$input-border-color: $jf-gray-60;
|
||||||
|
$input-placeholder-color: $text-muted;
|
||||||
|
|
||||||
|
$form-check-input-bg: $jf-gray-60;
|
||||||
|
$form-check-input-border: $jf-gray-50;
|
||||||
|
$form-check-input-checked-color: $jf-blue-focus;
|
||||||
|
$form-check-input-checked-bg-color: $jf-blue-hover;
|
||||||
|
|
||||||
|
$input-group-addon-bg: $input-bg;
|
||||||
|
|
||||||
|
$form-select-disabled-color: $jf-text-secondary;
|
||||||
|
$form-select-disabled-bg: $input-disabled-bg;
|
||||||
|
$form-select-indicator-color: $jf-gray-50;
|
||||||
|
|
||||||
|
$card-bg: $jf-gray-80;
|
||||||
|
$card-border-color: null;
|
||||||
|
|
||||||
|
$tooltip-color: $jf-text-bold;
|
||||||
|
$tooltip-bg: $jf-gray-50;
|
||||||
|
|
||||||
|
$modal-content-bg: $jf-gray-80;
|
||||||
|
$modal-content-border-color: $jf-gray-50;
|
||||||
|
$modal-header-border-color: null;
|
||||||
|
$modal-footer-border-color: null;
|
||||||
|
|
||||||
|
$list-group-bg: $card-bg;
|
||||||
|
$list-group-border-color: $jf-gray-50;
|
||||||
|
$list-group-hover-bg: $jf-blue-hover;
|
||||||
|
$list-group-active-bg: $jf-blue-focus;
|
||||||
|
$list-group-action-color: $jf-text-primary;
|
||||||
|
$list-group-action-hover-color: $jf-text-bold;
|
||||||
|
$list-group-action-active-color: $jf-text-bold;
|
||||||
|
$list-group-action-active-bg: $jf-blue-focus;
|
||||||
|
|
||||||
|
// idk why but i had to put these above and below the import
|
||||||
|
.list-group-item-danger {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-success {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
.btn-primary, .btn-outline-primary:hover, .btn-outline-primary:active {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $jf-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover, .close:active {
|
||||||
|
color: $jf-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-bright {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-danger {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-success {
|
||||||
|
color: $jf-text-bold;
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
|
10
scss/bs5/compile.sh
Executable file
10
scss/bs5/compile.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||||
|
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||||
|
sassc -t expanded -p 6 $1 $css_file
|
||||||
|
echo "Compiled."
|
||||||
|
postcss $css_file --replace --use autoprefixer
|
||||||
|
echo "Prefixed."
|
||||||
|
echo "Written to $css_file."
|
||||||
|
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||||
|
echo "Minified version written to $min_file."
|
Loading…
Reference in New Issue
Block a user