mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-19 16:51:13 +00:00
Compare commits
14 Commits
dynamic-se
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 9da3832e3a | |||
| dcdb02f9db | |||
| 4be88c4670 | |||
| 4fb99d1724 | |||
| adef32ef89 | |||
| ade935da4e | |||
| 81bb2520ad | |||
| acad3b1853 | |||
| d1cd83f5ff | |||
| 061fdd65bb | |||
| 34e58f5cb2 | |||
| fe12b7c4be | |||
| ac500e14cd | |||
| 46751e3743 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,13 +4,16 @@ MANIFEST.in
|
||||
dist/
|
||||
build/
|
||||
test.txt
|
||||
jellyfin_accounts/data/node_modules/
|
||||
node_modules/
|
||||
jellyfin_accounts/data/config-default.ini
|
||||
*.egg-info/
|
||||
pw-reset/
|
||||
jfa/
|
||||
colors.txt
|
||||
theme.css
|
||||
jellyfin_accounts/data/static/bootstrap-jf.css
|
||||
jellyfin_accounts/__pycache__/
|
||||
old/
|
||||
.jf-accounts/
|
||||
requirements.txt
|
||||
package-lock.json
|
||||
video/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 
|
||||
# 
|
||||
|
||||
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
|
||||
* 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)
|
||||
* 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)
|
||||
## Interface
|
||||
<p align="center">
|
||||
@@ -13,7 +13,7 @@ A basic account management system for [Jellyfin](https://github.com/jellyfin/jel
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/jfa.gif" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ server = http://jellyfin.local:8096
|
||||
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.
|
||||
client = jf-accounts
|
||||
version = 0.2.5
|
||||
version = 0.3.0
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.2.5
|
||||
device_id = jf-accounts-0.3.0
|
||||
|
||||
[ui]
|
||||
; 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
|
||||
host = 0.0.0.0
|
||||
port = 8056
|
||||
@@ -33,6 +35,8 @@ contact_message = Need help? contact me.
|
||||
help_message = Enter your details to create an account.
|
||||
; displayed when a user creates an account
|
||||
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 (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
|
||||
__version__ = "0.2.6"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
@@ -35,6 +35,9 @@ parser.add_argument(
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", "--install", help="attempt to install a system service.", action="store_true"
|
||||
)
|
||||
|
||||
args, leftovers = parser.parse_known_args()
|
||||
|
||||
@@ -70,10 +73,11 @@ else:
|
||||
temp_config = configparser.RawConfigParser()
|
||||
temp_config.read(config_path)
|
||||
|
||||
|
||||
def create_log(name):
|
||||
log = logging.getLogger(name)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
if temp_config.getboolean('ui', 'debug'):
|
||||
if temp_config.getboolean("ui", "debug"):
|
||||
log.setLevel(logging.DEBUG)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
@@ -89,6 +93,7 @@ def create_log(name):
|
||||
|
||||
log = create_log("main")
|
||||
|
||||
|
||||
def load_config(config_path, data_dir):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
@@ -137,8 +142,11 @@ def load_config(config_path, data_dir):
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
return config
|
||||
|
||||
|
||||
config = load_config(config_path, data_dir)
|
||||
|
||||
web_log = create_log("waitress")
|
||||
@@ -179,39 +187,45 @@ data_store = JSONStorage(
|
||||
config["files"]["user_configuration"],
|
||||
)
|
||||
|
||||
|
||||
def default_css():
|
||||
css = {}
|
||||
css[
|
||||
"href"
|
||||
] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
css[
|
||||
"integrity"
|
||||
] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
||||
css["crossorigin"] = "anonymous"
|
||||
return css
|
||||
if config.getboolean("ui", "bs5"):
|
||||
css_file = "bs5-jf.css"
|
||||
log.debug("Using Bootstrap 5")
|
||||
else:
|
||||
css_file = "bs4-jf.css"
|
||||
|
||||
|
||||
css = {}
|
||||
css = default_css()
|
||||
if "custom_css" in config["files"]:
|
||||
with open(config_base_path, "r") as f:
|
||||
themes = json.load(f)["ui"]["theme"]
|
||||
|
||||
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"] != "":
|
||||
try:
|
||||
shutil.copy(
|
||||
config["files"]["custom_css"], (local_dir / "static" / "bootstrap.css")
|
||||
)
|
||||
log.debug("Loaded custom CSS")
|
||||
css["href"] = "/bootstrap.css"
|
||||
css["integrity"] = ""
|
||||
css["crossorigin"] = ""
|
||||
css_path = Path(config["files"]["custom_css"])
|
||||
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||
log.debug('Loaded custom CSS "{css_path.name}"')
|
||||
css_file = css_path.name
|
||||
except FileNotFoundError:
|
||||
log.error(
|
||||
f'Custom CSS {config["files"]["custom_css"]} not found, using default.'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def resp(success=True, code=500):
|
||||
if success:
|
||||
r = jsonify({"success": True})
|
||||
@@ -226,7 +240,29 @@ def resp(success=True, code=500):
|
||||
|
||||
|
||||
def main():
|
||||
if args.get_defaults:
|
||||
if args.install:
|
||||
executable = sys.argv[0]
|
||||
print(f'Assuming executable path "{executable}".')
|
||||
options = ["systemd"]
|
||||
for i, opt in enumerate(options):
|
||||
print(f"{i+1}: {opt}")
|
||||
success = False
|
||||
while not success:
|
||||
try:
|
||||
method = options[int(input(">: ")) - 1]
|
||||
success = True
|
||||
except IndexError:
|
||||
pass
|
||||
if method == "systemd":
|
||||
with open(local_dir / "services" / "jf-accounts.service", "r") as f:
|
||||
data = f.read()
|
||||
data = data.replace("{executable}", executable)
|
||||
service_path = str(Path("jf-accounts.service").resolve())
|
||||
with open(service_path, "w") as f:
|
||||
f.write(data)
|
||||
print(f"service written to the current directory\n({service_path}).")
|
||||
print("Place this in the appropriate directory, and reload daemons.")
|
||||
elif args.get_defaults:
|
||||
import json
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
|
||||
|
||||
@@ -70,6 +70,19 @@
|
||||
"name": "General",
|
||||
"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": {
|
||||
"name": "Address",
|
||||
"required": true,
|
||||
@@ -150,6 +163,14 @@
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"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": {
|
||||
@@ -208,7 +229,7 @@
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
|
||||
8
jellyfin_accounts/data/services/jf-accounts.service
Normal file
8
jellyfin_accounts/data/services/jf-accounts.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=A basic account management system for Jellyfin.
|
||||
|
||||
[Service]
|
||||
ExecStart={executable}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<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="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Page not found.</h1>
|
||||
<p>
|
||||
{{ contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% 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 {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Page not found.</h1>
|
||||
<p>
|
||||
{{ contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,14 +12,18 @@
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
|
||||
<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>
|
||||
{% 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>
|
||||
.pageContainer {
|
||||
@@ -49,17 +53,11 @@
|
||||
margin-top: 5%;
|
||||
color: grey;
|
||||
}
|
||||
.fa-clipboard {
|
||||
color: grey;
|
||||
}
|
||||
.fa-clipboard:hover {
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<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-content">
|
||||
<div class="modal-header">
|
||||
@@ -82,7 +80,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
@@ -111,7 +109,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
@@ -130,7 +128,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
@@ -153,7 +151,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
@@ -176,13 +174,13 @@
|
||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<div class="card bg-light mb-3 linkGroup">
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
</ul>
|
||||
</div>
|
||||
<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-body">
|
||||
<form action="#" method="POST" id="inviteForm">
|
||||
@@ -200,16 +198,14 @@
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<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">
|
||||
</div>
|
||||
<div class="input-group-text">
|
||||
<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>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,6 +214,7 @@
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</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="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<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>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% 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>
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 20%;
|
||||
@@ -61,11 +67,11 @@
|
||||
<p class="contactBox">{{ contactMessage }}</p>
|
||||
<div class="container" id="container">
|
||||
<div class="row" id="cardContainer">
|
||||
<div class="col-sm" id="accountForm">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="col-sm">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST">
|
||||
<form action="#" method="POST" id="accountForm">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
@@ -80,7 +86,7 @@
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||
</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">
|
||||
<span id="createAccount">Create Account</span>
|
||||
</button>
|
||||
@@ -91,7 +97,7 @@
|
||||
</div>
|
||||
{% if validate %}
|
||||
<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-body">
|
||||
<ul class="list-group">
|
||||
@@ -108,7 +114,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></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();
|
||||
function toggleSpinner () {
|
||||
var submitButton = document.getElementById('submitButton');
|
||||
@@ -131,25 +155,24 @@
|
||||
}
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
$("form").submit(function() {
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
toggleSpinner();
|
||||
var send = $("form").serializeObject();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
{% if not username %}
|
||||
send['email'] = send['username'];
|
||||
{% endif %}
|
||||
send = JSON.stringify(send);
|
||||
$.ajax('/newUser', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
crossDomain : true,
|
||||
complete : function(response){
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/newUser", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = response['responseJSON'];
|
||||
var data = this.response;
|
||||
if ('error' in data) {
|
||||
var text = document.createTextNode(data['error']);
|
||||
// <div class="alert alert-danger" id="errorBox"></div>
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
@@ -177,13 +200,14 @@
|
||||
};
|
||||
};
|
||||
if (valid == true) {
|
||||
$('#successBox').modal('show');
|
||||
successBox.show();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Invalid Code</title>
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Invalid Code.</h1>
|
||||
<p>The above code is either incorrect, or has expired.</p>
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Invalid Code</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% 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 {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Invalid Code.</h1>
|
||||
<p>The above code is either incorrect, or has expired.</p>
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
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.web_api import config, checkInvite, validator
|
||||
|
||||
@@ -11,9 +11,8 @@ def page_not_found(e):
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
@@ -25,9 +24,8 @@ def admin():
|
||||
# return app.send_static_file('admin.html')
|
||||
return render_template(
|
||||
"admin.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage="",
|
||||
email_enabled=config.getboolean("invite_emails", "enabled"),
|
||||
)
|
||||
@@ -36,13 +34,18 @@ def admin():
|
||||
@app.route("/<path:path>")
|
||||
def static_proxy(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 (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
@@ -59,9 +62,8 @@ def inviteProxy(path):
|
||||
email = ""
|
||||
return render_template(
|
||||
"form.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
helpMessage=config["ui"]["help_message"],
|
||||
successMessage=config["ui"]["success_message"],
|
||||
@@ -77,8 +79,7 @@ def inviteProxy(path):
|
||||
log.debug("Attempted use of invalid invite")
|
||||
return render_template(
|
||||
"invalidCode.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
)
|
||||
|
||||
@@ -338,13 +338,9 @@ def modifyConfig():
|
||||
for section in data:
|
||||
if section in temp_config:
|
||||
for item in data[section]:
|
||||
if item in temp_config[section]:
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f"{section}/{item} modified")
|
||||
else:
|
||||
data[section][item] = False
|
||||
log.debug(f"{section}/{item} does not exist in config")
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f"{section}/{item} modified")
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
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]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.2.6"
|
||||
version = "0.3.0"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
@@ -9,7 +9,7 @@ homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||
keywords = ["jellyfin", "jf-accounts"]
|
||||
include = ["jellyfin_accounts/data/*"]
|
||||
exclude = ["images/*"]
|
||||
exclude = ["images/*", "scss/*"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
|
||||
147
requirements.txt
Normal file
147
requirements.txt
Normal file
@@ -0,0 +1,147 @@
|
||||
certifi==2020.4.5.2 \
|
||||
--hash=sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc \
|
||||
--hash=sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1
|
||||
cffi==1.14.0 \
|
||||
--hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \
|
||||
--hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \
|
||||
--hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c \
|
||||
--hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \
|
||||
--hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \
|
||||
--hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \
|
||||
--hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \
|
||||
--hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \
|
||||
--hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \
|
||||
--hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \
|
||||
--hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \
|
||||
--hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \
|
||||
--hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \
|
||||
--hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \
|
||||
--hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \
|
||||
--hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \
|
||||
--hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \
|
||||
--hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \
|
||||
--hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \
|
||||
--hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \
|
||||
--hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \
|
||||
--hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \
|
||||
--hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \
|
||||
--hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \
|
||||
--hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \
|
||||
--hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \
|
||||
--hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \
|
||||
--hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae
|
||||
click==7.1.2 \
|
||||
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
|
||||
--hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a
|
||||
cryptography==2.9.2 \
|
||||
--hash=sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e \
|
||||
--hash=sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b \
|
||||
--hash=sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365 \
|
||||
--hash=sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0 \
|
||||
--hash=sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55 \
|
||||
--hash=sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270 \
|
||||
--hash=sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf \
|
||||
--hash=sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d \
|
||||
--hash=sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785 \
|
||||
--hash=sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b \
|
||||
--hash=sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae \
|
||||
--hash=sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b \
|
||||
--hash=sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6 \
|
||||
--hash=sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3 \
|
||||
--hash=sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b \
|
||||
--hash=sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e \
|
||||
--hash=sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0 \
|
||||
--hash=sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5 \
|
||||
--hash=sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229
|
||||
flask==1.1.2 \
|
||||
--hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \
|
||||
--hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060
|
||||
flask-httpauth==3.3.0 \
|
||||
--hash=sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9 \
|
||||
--hash=sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72
|
||||
idna==2.9 \
|
||||
--hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa \
|
||||
--hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb
|
||||
itsdangerous==1.1.0 \
|
||||
--hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \
|
||||
--hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19
|
||||
jinja2==2.11.2 \
|
||||
--hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \
|
||||
--hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0
|
||||
markupsafe==1.1.1 \
|
||||
--hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
|
||||
--hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
|
||||
--hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
|
||||
--hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
|
||||
--hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
|
||||
--hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
|
||||
--hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
|
||||
--hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
|
||||
--hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
|
||||
--hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
|
||||
--hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
|
||||
--hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
|
||||
--hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
|
||||
--hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
|
||||
--hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
|
||||
--hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
|
||||
--hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
|
||||
--hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
|
||||
--hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
|
||||
--hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
|
||||
--hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
|
||||
--hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
|
||||
--hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
|
||||
--hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
|
||||
--hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
|
||||
--hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
|
||||
--hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
|
||||
--hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
|
||||
--hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
|
||||
--hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
|
||||
--hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
|
||||
--hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
|
||||
--hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b
|
||||
packaging==20.4 \
|
||||
--hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 \
|
||||
--hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8
|
||||
passlib==1.7.2 \
|
||||
--hash=sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177 \
|
||||
--hash=sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8
|
||||
pathtools==0.1.2 \
|
||||
--hash=sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0
|
||||
pycparser==2.20 \
|
||||
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \
|
||||
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0
|
||||
pyopenssl==19.1.0 \
|
||||
--hash=sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504 \
|
||||
--hash=sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507
|
||||
pyparsing==2.4.7 \
|
||||
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
|
||||
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
|
||||
python-dateutil==2.8.1 \
|
||||
--hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \
|
||||
--hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a
|
||||
pytz==2020.1 \
|
||||
--hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \
|
||||
--hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048
|
||||
requests==2.23.0 \
|
||||
--hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \
|
||||
--hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6
|
||||
six==1.15.0 \
|
||||
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
|
||||
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
|
||||
urllib3==1.25.9 \
|
||||
--hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 \
|
||||
--hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527
|
||||
waitress==1.4.4 \
|
||||
--hash=sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db \
|
||||
--hash=sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261
|
||||
watchdog==0.10.2 \
|
||||
--hash=sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b
|
||||
werkzeug==1.0.1 \
|
||||
--hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \
|
||||
--hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c
|
||||
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."
|
||||
Reference in New Issue
Block a user