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