mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-20 01:01:13 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27169e4e0d | |||
| db3b992857 | |||
| 89c132e92e | |||
| 7bda2f4141 | |||
| 71f05f2348 | |||
| 94e69ad090 | |||
| a3d3d97b3b | |||
| 781306f1ef | |||
| a62eab9565 | |||
| a2a2abc7f2 | |||
| fa0527c6a7 | |||
| b33922059c | |||
| 9da3832e3a |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,8 +12,10 @@ jfa/
|
|||||||
colors.txt
|
colors.txt
|
||||||
theme.css
|
theme.css
|
||||||
jellyfin_accounts/__pycache__/
|
jellyfin_accounts/__pycache__/
|
||||||
|
jellyfin_accounts/data/static/*.css
|
||||||
old/
|
old/
|
||||||
.jf-accounts/
|
.jf-accounts/
|
||||||
requirements.txt
|
requirements.txt
|
||||||
package-lock.json
|
|
||||||
video/
|
video/
|
||||||
|
scss/bs5/*.css*
|
||||||
|
scss/bs4/*.css*
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ 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.3.0
|
version = 0.3.2
|
||||||
device = jf-accounts
|
device = jf-accounts
|
||||||
device_id = jf-accounts-0.3.0
|
device_id = jf-accounts-0.3.2
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; settings related to the ui and program functionality.
|
; settings related to the ui and program functionality.
|
||||||
; choose the look of jellyfin-accounts.
|
; default appearance for all users.
|
||||||
theme = Jellyfin (Dark)
|
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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.6"
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import configparser
|
import configparser
|
||||||
@@ -218,7 +218,7 @@ elif "Custom" in current_theme and "custom_css" in config["files"]:
|
|||||||
try:
|
try:
|
||||||
css_path = Path(config["files"]["custom_css"])
|
css_path = Path(config["files"]["custom_css"])
|
||||||
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||||
log.debug('Loaded custom CSS "{css_path.name}"')
|
log.debug(f'Loaded custom CSS "{css_path.name}"')
|
||||||
css_file = css_path.name
|
css_file = css_path.name
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log.error(
|
log.error(
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
"description": "Settings related to the UI and program functionality."
|
"description": "Settings related to the UI and program functionality."
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"name": "Look",
|
"name": "Default Look",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "select",
|
"type": "select",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"Custom CSS"
|
"Custom CSS"
|
||||||
],
|
],
|
||||||
"value": "Jellyfin (Dark)",
|
"value": "Jellyfin (Dark)",
|
||||||
"description": "Choose the look of jellyfin-accounts."
|
"description": "Default appearance for all users."
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
"name": "Address",
|
"name": "Address",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ function serializeForm(id) {
|
|||||||
case 'password':
|
case 'password':
|
||||||
case 'select-one':
|
case 'select-one':
|
||||||
case 'email':
|
case 'email':
|
||||||
|
case 'number':
|
||||||
formData[name] = el.value;
|
formData[name] = el.value;
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ for (var i = 0; i < authRadios.length; i++) {
|
|||||||
checkAuthRadio();
|
checkAuthRadio();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkEmailRadio() {
|
function checkEmailRadio() {
|
||||||
document.getElementById('emailNextButton').href = '#page-5';
|
document.getElementById('emailNextButton').href = '#page-5';
|
||||||
document.getElementById('valBackButton').href = '#page-7';
|
document.getElementById('valBackButton').href = '#page-7';
|
||||||
@@ -35,6 +36,7 @@ for (var i = 0; i < emailRadios.length; i++) {
|
|||||||
checkEmailRadio();
|
checkEmailRadio();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkSSL() {
|
function checkSSL() {
|
||||||
var label = document.getElementById('emailSSL_TLSLabel');
|
var label = document.getElementById('emailSSL_TLSLabel');
|
||||||
if (document.getElementById('emailSSL_TLS').checked) {
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
@@ -101,16 +103,15 @@ document.getElementById('jfTestButton').onclick = function() {
|
|||||||
jfData['jfHost'] = document.getElementById('jfHost').value;
|
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||||
jfData['jfUser'] = document.getElementById('jfUser').value;
|
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||||
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||||
$.ajax('/testJF', {
|
var req = new XMLHttpRequest();
|
||||||
type : 'POST',
|
req.open("POST", "/testJF", true);
|
||||||
dataType : 'json',
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
contentType : 'application/json',
|
req.responseType = 'json';
|
||||||
data : JSON.stringify(jfData),
|
req.onreadystatechange = function() {
|
||||||
complete: function(response) {
|
if (this.readyState == 4) {
|
||||||
testButton.disabled = false;
|
testButton.disabled = false;
|
||||||
testButton.className = '';
|
testButton.className = '';
|
||||||
var success = response['responseJSON']['success'];
|
if (this.response['success'] == true) {
|
||||||
if (success == true) {
|
|
||||||
testButton.classList.add('btn', 'btn-success');
|
testButton.classList.add('btn', 'btn-success');
|
||||||
testButton.textContent = 'Success';
|
testButton.textContent = 'Success';
|
||||||
nextButton.classList.remove('disabled');
|
nextButton.classList.remove('disabled');
|
||||||
@@ -118,9 +119,10 @@ document.getElementById('jfTestButton').onclick = function() {
|
|||||||
} else {
|
} else {
|
||||||
testButton.classList.add('btn', 'btn-danger');
|
testButton.classList.add('btn', 'btn-danger');
|
||||||
testButton.textContent = 'Failed';
|
testButton.textContent = 'Failed';
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
});
|
};
|
||||||
|
req.send(JSON.stringify(jfData));
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('submitButton').onclick = function() {
|
document.getElementById('submitButton').onclick = function() {
|
||||||
@@ -158,7 +160,7 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
if (document.getElementById('emailDisabledRadio').checked) {
|
if (document.getElementById('emailDisabledRadio').checked) {
|
||||||
config['password_resets']['enabled'] = 'false';
|
config['password_resets']['enabled'] = 'false';
|
||||||
config['invite_emails']['enabled'] = 'false';
|
config['invite_emails']['enabled'] = 'false';
|
||||||
} else {
|
} else {
|
||||||
if (document.getElementById('emailSMTPRadio').checked) {
|
if (document.getElementById('emailSMTPRadio').checked) {
|
||||||
if (document.getElementById('emailSSL_TLS').checked) {
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
config['smtp']['encryption'] = 'ssl_tls';
|
config['smtp']['encryption'] = 'ssl_tls';
|
||||||
@@ -217,18 +219,18 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
||||||
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
||||||
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
||||||
console.log(config);
|
// Send it
|
||||||
$.ajax('/modifyConfig', {
|
var req = new XMLHttpRequest();
|
||||||
type : 'POST',
|
req.open("POST", "/modifyConfig", true);
|
||||||
dataType : 'json',
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
contentType : 'application/json',
|
req.responseType = 'json';
|
||||||
data : JSON.stringify(config),
|
req.onreadystatechange = function() {
|
||||||
complete: function(response) {
|
if (this.readyState == 4) {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.className = '';
|
submitButton.className = '';
|
||||||
submitButton.classList.add('btn', 'btn-success');
|
submitButton.classList.add('btn', 'btn-success');
|
||||||
submitButton.textContent = 'Success';
|
submitButton.textContent = 'Success';
|
||||||
}
|
};
|
||||||
});
|
};
|
||||||
|
req.send(JSON.stringify(config));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,42 @@
|
|||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
<script>
|
||||||
|
function getCookie(cname) {
|
||||||
|
var name = cname + "=";
|
||||||
|
var decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
var ca = decodedCookie.split(';');
|
||||||
|
for(var i = 0; i < ca.length; i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while (c.charAt(0) == ' ') {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
{% if bs5 %}
|
||||||
|
const bsVersion = 5;
|
||||||
|
{% else %}
|
||||||
|
const bsVersion = 4;
|
||||||
|
{% endif %}
|
||||||
|
console.log('create');
|
||||||
|
var css = document.createElement('link');
|
||||||
|
css.setAttribute('rel', 'stylesheet');
|
||||||
|
css.setAttribute('type', 'text/css');
|
||||||
|
var cssCookie = getCookie("css");
|
||||||
|
if (cssCookie.includes('bs' + bsVersion)) {
|
||||||
|
console.log('href');
|
||||||
|
css.setAttribute('href', cssCookie);
|
||||||
|
} else {
|
||||||
|
console.log('href');
|
||||||
|
css.setAttribute('href', '{{ css_file }}');
|
||||||
|
};
|
||||||
|
console.log('append');
|
||||||
|
document.head.appendChild(css);
|
||||||
|
</script>
|
||||||
{% if not bs5 %}
|
{% 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>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -53,10 +88,42 @@
|
|||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
.circle {
|
||||||
|
/*margin-left: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 5000;*/
|
||||||
|
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||||
|
}
|
||||||
|
.smooth-transition {
|
||||||
|
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||||
|
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||||
|
}
|
||||||
|
.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-rotated {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||||
|
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Admin</title>
|
<title>Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="smooth-transition">
|
||||||
<div class="modal fade" id="login" 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">
|
||||||
@@ -171,9 +238,11 @@
|
|||||||
<h1>
|
<h1>
|
||||||
Accounts admin
|
Accounts admin
|
||||||
</h1>
|
</h1>
|
||||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
<div class="btn-group" role="group" id="headerButtons">
|
||||||
Settings <i class="fa fa-cog"></i>
|
<button type="button" class="btn btn-primary" id="openSettings">
|
||||||
</button>
|
Settings <i class="fa fa-cog"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="card 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">
|
||||||
@@ -183,29 +252,66 @@
|
|||||||
<div class="card 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" class="container">
|
||||||
<div class="form-group">
|
<div class="row align-items-start">
|
||||||
<label for="hours">Hours</label>
|
<div class="col">
|
||||||
<select class="form-control" id="hours" name="hours">
|
<div class="form-group">
|
||||||
</select>
|
<label for="days">Days</label>
|
||||||
</div>
|
<select class="form-control form-select" id="days" name="days">
|
||||||
<div class="form-group">
|
</select>
|
||||||
<label for="minutes">Minutes</label>
|
</div>
|
||||||
<select class="form-control" id="minutes" name="minutes">
|
<div class="form-group">
|
||||||
</select>
|
<label for="hours">Hours</label>
|
||||||
</div>
|
<select class="form-control form-select" id="hours" name="hours">
|
||||||
{% if email_enabled %}
|
</select>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="send_to_address">Send invite to address</label>
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="minutes">Minutes</label>
|
||||||
<div class="input-group-text">
|
<select class="form-control form-select" id="minutes" name="minutes">
|
||||||
<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">
|
</select>
|
||||||
</div>
|
|
||||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="col">
|
||||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
<div class="form-group">
|
||||||
|
<label for="multiUseCount">
|
||||||
|
Multiple uses
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||||
|
</div>
|
||||||
|
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
||||||
|
<label class="form-check-label" for="noUseLimit">
|
||||||
|
No use limit
|
||||||
|
</label>
|
||||||
|
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||||
|
</div>
|
||||||
|
{% if email_enabled %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="send_to_address">Send invite to address</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||||
|
</div>
|
||||||
|
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-group d-flex float-right">
|
||||||
|
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,9 @@
|
|||||||
submitButton.replaceChild(newSpan, oldSpan);
|
submitButton.replaceChild(newSpan, oldSpan);
|
||||||
};
|
};
|
||||||
document.getElementById('accountForm').onsubmit = function() {
|
document.getElementById('accountForm').onsubmit = function() {
|
||||||
|
if (document.getElementById('errorMessage')) {
|
||||||
|
document.getElementById('errorMessage').remove();
|
||||||
|
}
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var send = serializeForm('accountForm');
|
var send = serializeForm('accountForm');
|
||||||
send['code'] = code;
|
send['code'] = code;
|
||||||
@@ -171,12 +174,18 @@
|
|||||||
if (this.readyState == 4) {
|
if (this.readyState == 4) {
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var data = this.response;
|
var data = this.response;
|
||||||
if ('error' in data) {
|
if ('error' in data || data['success'] == false) {
|
||||||
var text = document.createTextNode(data['error']);
|
if (typeof(data['error']) != 'undefined') {
|
||||||
|
var errorMessage = data['error'];
|
||||||
|
} else {
|
||||||
|
var errorMessage = 'Unknown Error';
|
||||||
|
}
|
||||||
|
var text = document.createTextNode(errorMessage);
|
||||||
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', '');
|
||||||
error.appendChild(text);
|
error.appendChild(text);
|
||||||
|
error.id = 'errorMessage';
|
||||||
document.getElementById('errorBox').appendChild(error);
|
document.getElementById('errorBox').appendChild(error);
|
||||||
} else {
|
} else {
|
||||||
var valid = true
|
var valid = true
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class Jellyfin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def newUser(self, username: str, password: str):
|
def newUser(self, username: str, password: str):
|
||||||
for user in self.getUsers():
|
for user in self.getUsers(public=False):
|
||||||
if user["Name"] == username:
|
if user["Name"] == username:
|
||||||
raise self.UserExistsError
|
raise self.UserExistsError
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
if config.getboolean("ui", "bs5"):
|
||||||
|
bsVersion = 5
|
||||||
|
else:
|
||||||
|
bsVersion = 4
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
return (
|
return (
|
||||||
@@ -35,11 +41,11 @@ def admin():
|
|||||||
def static_proxy(path):
|
def static_proxy(path):
|
||||||
if "html" not in path:
|
if "html" not in path:
|
||||||
if "admin.js" in path:
|
if "admin.js" in path:
|
||||||
if config.getboolean("ui", "bs5"):
|
return (
|
||||||
bsVersion = 5
|
render_template("admin.js", bsVersion=bsVersion, css_file=css_file),
|
||||||
else:
|
200,
|
||||||
bsVersion = 4
|
{"Content-Type": "text/javascript"},
|
||||||
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(
|
||||||
|
|||||||
@@ -20,21 +20,49 @@ from jellyfin_accounts import web_log as log
|
|||||||
from jellyfin_accounts.validate_password import PasswordValidator
|
from jellyfin_accounts.validate_password import PasswordValidator
|
||||||
|
|
||||||
|
|
||||||
def checkInvite(code, delete=False):
|
def format_datetime(dt):
|
||||||
|
result = dt.strftime(config["email"]["date_format"])
|
||||||
|
if config.getboolean("email", "use_24h"):
|
||||||
|
result += f' {dt.strftime("%H:%M")}'
|
||||||
|
else:
|
||||||
|
result += f' {dt.strftime("%I:%M %p")}'
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def checkInvite(code, used=False, username=None):
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
invites = dict(data_store.invites)
|
invites = dict(data_store.invites)
|
||||||
match = False
|
match = False
|
||||||
for invite in invites:
|
for invite in invites:
|
||||||
|
if (
|
||||||
|
"remaining-uses" not in invites[invite]
|
||||||
|
and "no-limit" not in invites[invite]
|
||||||
|
):
|
||||||
|
invites[invite]["remaining-uses"] = 1
|
||||||
expiry = datetime.datetime.strptime(
|
expiry = datetime.datetime.strptime(
|
||||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||||
)
|
)
|
||||||
if current_time >= expiry:
|
if current_time >= expiry or (
|
||||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
||||||
|
):
|
||||||
|
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
||||||
del data_store.invites[invite]
|
del data_store.invites[invite]
|
||||||
elif invite == code:
|
elif invite == code:
|
||||||
match = True
|
match = True
|
||||||
if delete:
|
if used:
|
||||||
del data_store.invites[code]
|
delete = False
|
||||||
|
inv = dict(data_store.invites[code])
|
||||||
|
if "used-by" not in inv:
|
||||||
|
inv["used-by"] = []
|
||||||
|
if "remaining-uses" in inv:
|
||||||
|
if inv["remaining-uses"] == 1:
|
||||||
|
delete = True
|
||||||
|
del data_store.invites[code]
|
||||||
|
elif "no-limit" not in invites[invite]:
|
||||||
|
inv["remaining-uses"] -= 1
|
||||||
|
inv["used-by"].append([username, format_datetime(current_time)])
|
||||||
|
if not delete:
|
||||||
|
data_store.invites[code] = inv
|
||||||
return match
|
return match
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +185,7 @@ def newUser():
|
|||||||
return jsonify({"error": error})
|
return jsonify({"error": error})
|
||||||
except:
|
except:
|
||||||
return jsonify({"error": "Unknown error"})
|
return jsonify({"error": "Unknown error"})
|
||||||
checkInvite(data["code"], delete=True)
|
checkInvite(data["code"], used=True, username=data["username"])
|
||||||
if user.status_code == 200:
|
if user.status_code == 200:
|
||||||
try:
|
try:
|
||||||
policy = data_store.user_template
|
policy = data_store.user_template
|
||||||
@@ -200,9 +228,19 @@ def newUser():
|
|||||||
def generateInvite():
|
def generateInvite():
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
|
delta = datetime.timedelta(
|
||||||
|
days=int(data["days"]), hours=int(data["hours"]), minutes=int(data["minutes"])
|
||||||
|
)
|
||||||
invite_code = secrets.token_urlsafe(16)
|
invite_code = secrets.token_urlsafe(16)
|
||||||
invite = {}
|
invite = {}
|
||||||
|
invite["created"] = format_datetime(current_time)
|
||||||
|
if data["multiple-uses"]:
|
||||||
|
if data["no-limit"]:
|
||||||
|
invite["no-limit"] = True
|
||||||
|
else:
|
||||||
|
invite["remaining-uses"] = int(data["remaining-uses"])
|
||||||
|
else:
|
||||||
|
invite["remaining-uses"] = 1
|
||||||
log.debug(f"Creating new invite: {invite_code}")
|
log.debug(f"Creating new invite: {invite_code}")
|
||||||
valid_till = current_time + delta
|
valid_till = current_time + delta
|
||||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||||
@@ -245,9 +283,20 @@ def getInvites():
|
|||||||
valid_for = expiry - current_time
|
valid_for = expiry - current_time
|
||||||
invite = {
|
invite = {
|
||||||
"code": code,
|
"code": code,
|
||||||
|
"days": valid_for.days,
|
||||||
"hours": valid_for.seconds // 3600,
|
"hours": valid_for.seconds // 3600,
|
||||||
"minutes": (valid_for.seconds // 60) % 60,
|
"minutes": (valid_for.seconds // 60) % 60,
|
||||||
}
|
}
|
||||||
|
if "created" in invites[code]:
|
||||||
|
invite["created"] = invites[code]["created"]
|
||||||
|
if "used-by" in invites[code]:
|
||||||
|
invite["used-by"] = invites[code]["used-by"]
|
||||||
|
if "no-limit" in invites[code]:
|
||||||
|
invite["no-limit"] = invites[code]["no-limit"]
|
||||||
|
if "remaining-uses" in invites[code]:
|
||||||
|
invite["remaining-uses"] = invites[code]["remaining-uses"]
|
||||||
|
else:
|
||||||
|
invite["remaining-uses"] = 1
|
||||||
if "email" in invites[code]:
|
if "email" in invites[code]:
|
||||||
invite["email"] = invites[code]["email"]
|
invite["email"] = invites[code]["email"]
|
||||||
response["invites"].append(invite)
|
response["invites"].append(invite)
|
||||||
|
|||||||
2553
package-lock.json
generated
2553
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "jellyfin-accounts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "This is only used for grabbing scss build dependencies, and isn't a real package.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/hrfee/jellyfin-accounts.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/hrfee/jellyfin-accounts/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^9.8.4",
|
||||||
|
"bootstrap": "^5.0.0-alpha1",
|
||||||
|
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||||
|
"clean-css-cli": "^4.3.0",
|
||||||
|
"postcss-cli": "^7.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
poetry.lock
generated
43
poetry.lock
generated
@@ -162,6 +162,17 @@ MarkupSafe = ">=0.23"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=0.8)"]
|
i18n = ["Babel (>=0.8)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Sass for Python: A straightforward binding of libsass for Python."
|
||||||
|
name = "libsass"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "0.20.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
@@ -332,6 +343,17 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "tasks runner for python projects"
|
||||||
|
name = "taskipy"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6,<4.0"
|
||||||
|
version = "1.2.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
toml = ">=0.10.0,<0.11.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
@@ -400,7 +422,7 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
|
|||||||
watchdog = ["watchdog"]
|
watchdog = ["watchdog"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "847ce2a6a3927efdfb3b78935b348e9b4dc63d7e60959af6cc8b9fbc5a24567b"
|
content-hash = "fa8b5fb1ded41b673b8062a2bfc6467e6a484ff62b578147bec001d7d9d8ca16"
|
||||||
python-versions = "^3.6"
|
python-versions = "^3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
@@ -518,6 +540,21 @@ jinja2 = [
|
|||||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||||
]
|
]
|
||||||
|
libsass = [
|
||||||
|
{file = "libsass-0.20.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726"},
|
||||||
|
{file = "libsass-0.20.0-cp27-cp27m-win32.whl", hash = "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57"},
|
||||||
|
{file = "libsass-0.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb"},
|
||||||
|
{file = "libsass-0.20.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481"},
|
||||||
|
{file = "libsass-0.20.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd"},
|
||||||
|
{file = "libsass-0.20.0-cp36-cp36m-win32.whl", hash = "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60"},
|
||||||
|
{file = "libsass-0.20.0-cp36-cp36m-win_amd64.whl", hash = "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1"},
|
||||||
|
{file = "libsass-0.20.0-cp37-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a"},
|
||||||
|
{file = "libsass-0.20.0-cp37-cp37m-win32.whl", hash = "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d"},
|
||||||
|
{file = "libsass-0.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687"},
|
||||||
|
{file = "libsass-0.20.0-cp38-cp38-win32.whl", hash = "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7"},
|
||||||
|
{file = "libsass-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b"},
|
||||||
|
{file = "libsass-0.20.0.tar.gz", hash = "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc"},
|
||||||
|
]
|
||||||
markupsafe = [
|
markupsafe = [
|
||||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||||
@@ -645,6 +682,10 @@ six = [
|
|||||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||||
]
|
]
|
||||||
|
taskipy = [
|
||||||
|
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
|
||||||
|
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
|
||||||
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
||||||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jellyfin-accounts"
|
name = "jellyfin-accounts"
|
||||||
version = "0.3.0"
|
version = "0.3.6"
|
||||||
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>"]
|
||||||
@@ -8,7 +8,7 @@ license = "MIT"
|
|||||||
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
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/*", "jellyfin_accounts/data/static/*.css"]
|
||||||
exclude = ["images/*", "scss/*"]
|
exclude = ["images/*", "scss/*"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
@@ -16,7 +16,6 @@ classifiers = [
|
|||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.6"
|
python = "^3.6"
|
||||||
pyopenssl = "^19.1.0"
|
pyopenssl = "^19.1.0"
|
||||||
@@ -34,11 +33,17 @@ packaging = "^20.4"
|
|||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
neovim = "^0.3.1"
|
neovim = "^0.3.1"
|
||||||
black = "^19.10b0"
|
black = "^19.10b0"
|
||||||
|
taskipy = "^1.2.1"
|
||||||
|
libsass = "^0.20.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
jf-accounts = 'jellyfin_accounts:main'
|
jf-accounts = 'jellyfin_accounts:main'
|
||||||
|
|
||||||
|
[tool.taskipy.tasks]
|
||||||
|
pre_compile-css = "task get-npm-deps"
|
||||||
|
compile-css = "python scss/compile.py"
|
||||||
|
get-npm-deps = "python scss/get_node_deps.py"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.masonry.api"
|
||||||
|
|||||||
@@ -2,15 +2,9 @@
|
|||||||
|
|
||||||
* `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.
|
* `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.
|
**Note**: It is assumed that Bootstrap 5 is installed in `../../node_modules/bootstrap` relative to itself, and Bootstrap 4 in `../../node_modules/bootstrap4`.
|
||||||
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.
|
* Compilation requires dev dependencies (`poetry update`), bootstrap and some extra npm packages.
|
||||||
* 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.
|
* If you're buildings from source, you can simply run `poetry run task compile-css` before building to automatically get deps and compile CSS.
|
||||||
* For `node-sass`, replace the `sassc` line in `compile.sh` with
|
* If you are creating custom css, run `poetry run task get-npm-deps` to only install the necessary dependencies. Follow along with the commands `scss/compile.py` runs to build your css and then set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||||
```
|
|
||||||
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
9548
scss/bs4/bs4-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs4/bs4-jf.min.css
vendored
7
scss/bs4/bs4-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.text-bright {
|
.text-bright {
|
||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/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
9438
scss/bs5/bs5-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs5/bs5-jf.min.css
vendored
7
scss/bs5/bs5-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button:active {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.text-bright {
|
.text-bright {
|
||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/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."
|
|
||||||
35
scss/compile.py
Executable file
35
scss/compile.py
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sass
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def runcmd(cmd):
|
||||||
|
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||||
|
return proc.communicate()
|
||||||
|
|
||||||
|
local_path = Path(__file__).resolve().parent
|
||||||
|
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||||
|
|
||||||
|
for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
|
||||||
|
scss = bsv / f'{bsv.name}-jf.scss'
|
||||||
|
css = bsv / f'{bsv.name}-jf.css'
|
||||||
|
min_css = bsv.parents[1] / 'jellyfin_accounts' / 'data' / 'static' / f'{bsv.name}-jf.css'
|
||||||
|
with open(css, 'w') as f:
|
||||||
|
f.write(sass.compile(filename=str(scss.resolve()),
|
||||||
|
output_style='expanded',
|
||||||
|
precision=6))
|
||||||
|
if css.exists():
|
||||||
|
print(f'{bsv.name}: Compiled.')
|
||||||
|
runcmd(f'{str((node_bin / "postcss").resolve())} {str(css.resolve())} --replace --use autoprefixer')
|
||||||
|
print(f'{bsv.name}: Prefixed.')
|
||||||
|
runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}')
|
||||||
|
if min_css.exists():
|
||||||
|
print(f'{bsv.name}: Minified and copied to {str(min_css.resolve())}.')
|
||||||
|
|
||||||
|
for v in [('bootstrap', 'bs5'), ('bootstrap4', 'bs4')]:
|
||||||
|
new_path = str((local_path.parent / 'jellyfin_accounts' / 'data' / 'static' / (v[1] + '.css')).resolve())
|
||||||
|
shutil.copy(str((local_path.parent / 'node_modules' / v[0] / 'dist' / 'css' / 'bootstrap.min.css').resolve()),
|
||||||
|
new_path)
|
||||||
|
print(f'Copied {v[1]} to {new_path}')
|
||||||
|
|
||||||
17
scss/get_node_deps.py
Normal file
17
scss/get_node_deps.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def runcmd(cmd):
|
||||||
|
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||||
|
return proc.communicate()
|
||||||
|
|
||||||
|
print('Installing npm packages')
|
||||||
|
|
||||||
|
root_path = Path(__file__).parents[1]
|
||||||
|
runcmd(f'npm install --prefix {root_path}')
|
||||||
|
|
||||||
|
if (root_path / 'node_modules' / 'cleancss').exists():
|
||||||
|
print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.')
|
||||||
|
|
||||||
Reference in New Issue
Block a user