Added theme toggle to Admin page

The admin can switch between the two default themes without a page
reload, with a nice animation (on small screens). Preference is stored
as a cookie, so the default theme setting will still apply to others.
This commit is contained in:
Harvey Tindall 2020-07-06 20:53:14 +01:00
parent 781306f1ef
commit a3d3d97b3b
7 changed files with 165 additions and 26 deletions

View File

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

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
__version__ = "0.3.2" __version__ = "0.3.3"
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(

View File

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

View File

@ -14,7 +14,39 @@
<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 %}
var bsVersion = 5;
{% else %}
var bsVersion = 4;
{% endif %}
var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css');
var cssCookie = getCookie("css");
if (cssCookie.includes('bs' + bsVersion)) {
css.setAttribute('href', cssCookie);
} else {
css.setAttribute('href', '{{ css_file }}');
};
document.head.appendChild(css);
// document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0].href = cssCookie;
</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 +85,27 @@
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 */
}
</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 +220,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">

View File

@ -1,5 +1,96 @@
var bsVersion = {{ bsVersion }}; 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 "";
}
function whichTransitionEvent(){
var t;
var el = document.createElement('fakeelement');
var transitions = {
'transition':'transitionend',
'OTransition':'oTransitionEnd',
'MozTransition':'transitionend',
'WebkitTransition':'webkitTransitionEnd'
};
for(t in transitions){
if( el.style[t] !== undefined ){
return transitions[t];
};
};
};
function toggleCSS() {
var cssEl = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0];
if (cssEl.href.includes("bs" + bsVersion + "-jf")) {
var href = "bs" + bsVersion + ".css";
} else {
var href = "bs" + bsVersion + "-jf.css";
};
cssEl.href = href;
document.cookie = "css=" + href;
};
var buttonWidth = 0;
var inTransition = false;
function toggleCSSAnim(el) {
var switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
if (window.innerWidth < 1500) {
var radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
var currentRadius = el.getBoundingClientRect().width / 2;
var scale = radius / currentRadius;
buttonWidth = window.getComputedStyle(el, null).width;
document.body.classList.remove('smooth-transition');
el.style.transform = 'scale(' + scale + ')';
el.style.color = switchToColor;
var transitionEnd = whichTransitionEvent();
el.addEventListener(transitionEnd, function() {
if (this.style.transform.length != 0) {
toggleCSS();
this.style.removeProperty('transform');
document.body.classList.add('smooth-transition');
};
}, false);
} else {
toggleCSS();
el.style.color = switchToColor;
};
};
var cssFile = "{{ css_file }}";
var buttonColor = 'custom';
if (cssFile.includes('jf')) {
buttonColor = 'rgb(255,255,255)';
} else if (cssFile.length == 7) {
buttonColor = 'rgb(16,16,16)';
}
if (buttonColor != 'custom') {
var fakeButton = document.createElement('i');
fakeButton.classList.add('fa', 'fa-circle', 'circle');
// fakeButton.style.color = buttonColor;
// fakeButton.style.marginLeft = '2rem;'
fakeButton.style = 'color: ' + buttonColor + '; margin-left: 0.4rem;';
fakeButton.id = 'fakeButton';
var switchButton = document.createElement('button');
switchButton.classList.add('btn', 'btn-secondary');
switchButton.textContent = 'Theme';
switchButton.onclick = function() {
var fb = document.getElementById('fakeButton')
toggleCSSAnim(fb);
};
var group = document.getElementById('headerButtons');
switchButton.appendChild(fakeButton);
group.appendChild(switchButton);
};
var bsVersion = {{ bsVersion }};
if (bsVersion == 5) { if (bsVersion == 5) {
function createModal(id, find = false) { function createModal(id, find = false) {
if (find) { if (find) {
@ -22,13 +113,6 @@ if (bsVersion == 5) {
}); });
}); });
}; };
// 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) { } else if (bsVersion == 4) {
document.getElementById('send_to_address_enabled').classList.remove('form-check-input'); document.getElementById('send_to_address_enabled').classList.remove('form-check-input');
function createModal(id, find = false) { function createModal(id, find = false) {

View File

@ -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,12 +41,10 @@ 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"):
bsVersion = 5
else:
bsVersion = 4
return ( return (
render_template("admin.js", bsVersion=bsVersion), render_template("admin.js",
bsVersion=bsVersion,
css_file=css_file),
200, 200,
{"Content-Type": "text/javascript"}, {"Content-Type": "text/javascript"},
) )

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "jellyfin-accounts" name = "jellyfin-accounts"
version = "0.3.2" version = "0.3.3"
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>"]