add dark mode support

This commit is contained in:
Eduardo Silva
2026-03-17 12:04:11 -03:00
parent 46b383a0b6
commit e91bc48168
12 changed files with 315 additions and 52 deletions

View File

@@ -27,3 +27,99 @@ body.layout-boxed .wrapper {
50% { opacity: 0.3; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
/* ── Peer list ── */
.peer-extra-info {
display: none;
}
.callout.position-relative {
padding: 0 !important;
}
#inviteTextContainer {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
#inviteText {
white-space: pre-line;
text-align: left;
}
.div-peer-text-information {
top: 0;
left: 0;
background: linear-gradient(to right, white, transparent);
width: 100%;
height: 100%;
}
.peer-lock-icon {
position: absolute;
right: 15px;
top: 48px;
opacity: 0.75;
}
/* ── Dark mode ── */
body.dark-mode.layout-boxed {
background: radial-gradient(circle at center, #1c2025 0%, #272c31 60%, #2d3238 100%);
}
body.dark-mode.layout-boxed .wrapper {
background-color: #343a40;
box-shadow: 0 0 20px rgba(0,0,0,0.6);
}
body.dark-mode pre {
background-color: #2d3035;
color: #dee2e6;
border: 1px solid #4b545c;
}
body.dark-mode .btn-primary {
background-color: #3d8bcd;
border-color: #3d8bcd;
color: #fff;
}
body.dark-mode .btn-primary:hover,
body.dark-mode .btn-primary:focus {
background-color: #4d9eff;
border-color: #4d9eff;
color: #fff;
}
body.dark-mode .btn-outline-primary {
color: #3d8bcd;
border-color: #3d8bcd;
}
body.dark-mode .btn-outline-primary:hover,
body.dark-mode .btn-outline-primary:focus {
background-color: #3d8bcd;
border-color: #3d8bcd;
color: #fff;
}
body.dark-mode .text-primary {
color: #3d8bcd !important;
}
body.dark-mode a {
color: #3d8bcd;
}
body.dark-mode a:hover {
color: #4d9eff;
}
body.dark-mode .card-primary.card-outline {
border-top-color: #3d8bcd;
}
body.dark-mode .nav-pills .nav-link.active,
body.dark-mode .nav-pills .show > .nav-link {
background-color: #3d8bcd;
}
body.dark-mode .div-peer-text-information {
background: linear-gradient(to right, #3f474e, transparent);
}
body.dark-mode .div-peer-text-information h5 a {
color: #dee2e6;
}
body.dark-mode .div-peer-text-information h5 a:hover {
color: #fff;
}

View File

@@ -31,12 +31,16 @@
<div class="col-md-6">
<button type="submit" class="btn btn-primary btn-block">{% trans 'Login' %}</button>
</div>
<div class="col-md-6">
<a class="btn btn-outline-primary btn-block" href="/change_language/">
{% trans 'Language' %}
<div class="col-md-3">
<a class="btn btn-outline-primary btn-block" href="/change_language/" title="{% trans 'Change Language' %}">
<i class="fas fa-language"></i>
</a>
</div>
<div class="col-md-3">
<a class="btn btn-outline-primary btn-block" href="#" id="darkModeToggle" title="{% trans 'Toggle Dark Mode' %}">
<i class="fas {% if request.COOKIES.darkMode == 'dark' %}fa-sun{% else %}fa-moon{% endif %}" id="darkModeIcon"></i>
</a>
</div>
</div>
</form>
</div>

View File

@@ -10,7 +10,15 @@
{% block page_custom_head %}{% endblock %}
</head>
<body class="hold-transition sidebar-mini layout-boxed {% if request.COOKIES.sidebarState == 'collapsed' %}sidebar-collapse{% endif %}">
<body class="hold-transition sidebar-mini layout-boxed {% if request.COOKIES.sidebarState == 'collapsed' %}sidebar-collapse{% endif %} {% if request.COOKIES.darkMode == 'dark' %}dark-mode{% endif %}">
<script>
(function() {
var m = document.cookie.match(/(?:^|; )darkMode=([^;]*)/);
if (!m && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-mode');
}
})();
</script>
<div class="wrapper">
<!-- Preloader -->

View File

@@ -15,8 +15,66 @@
<link rel="stylesheet" href="/static/AdminLTE-3.2.0/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="/static/AdminLTE-3.2.0/dist/css/adminlte.min.css">
<style>
body.dark-mode.register-page {
background: #1c2025;
}
body.dark-mode .register-box .card {
background-color: #343a40;
color: #dee2e6;
}
body.dark-mode .register-box .card .form-control {
background-color: #2d3238;
border-color: #4b545c;
color: #dee2e6;
}
body.dark-mode .register-box .card .form-control::placeholder {
color: #8d9aaa;
}
body.dark-mode .register-box .card .input-group-text {
background-color: #3f474e;
border-color: #4b545c;
color: #dee2e6;
}
body.dark-mode .register-box .register-logo a {
color: #dee2e6;
}
body.dark-mode .btn-primary {
background-color: #3d8bcd;
border-color: #3d8bcd;
color: #fff;
}
body.dark-mode .btn-primary:hover,
body.dark-mode .btn-primary:focus {
background-color: #4d9eff;
border-color: #4d9eff;
color: #fff;
}
body.dark-mode .btn-outline-primary {
color: #3d8bcd;
border-color: #3d8bcd;
}
body.dark-mode .btn-outline-primary:hover,
body.dark-mode .btn-outline-primary:focus {
background-color: #3d8bcd;
border-color: #3d8bcd;
color: #fff;
}
body.dark-mode .login-box-msg {
color: #dee2e6;
}
</style>
</head>
<body class="hold-transition register-page">
<body class="hold-transition register-page {% if request.COOKIES.darkMode == 'dark' %}dark-mode{% endif %}">
<script>
(function() {
var m = document.cookie.match(/(?:^|; )darkMode=([^;]*)/);
if (!m && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-mode');
}
})();
</script>
{% block content %}
{% endblock %}
@@ -26,6 +84,64 @@
<script src="/static/AdminLTE-3.2.0/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="/static/AdminLTE-3.2.0/dist/js/adminlte.min.js"></script>
<script>
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function syncDarkModeUI() {
var isDark = $('body').hasClass('dark-mode');
$('#darkModeIcon').toggleClass('fa-sun', isDark).toggleClass('fa-moon', !isDark);
}
$(document).ready(function() {
syncDarkModeUI();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!getCookie('darkMode')) {
if (e.matches) {
$('body').addClass('dark-mode');
} else {
$('body').removeClass('dark-mode');
}
syncDarkModeUI();
}
});
}
$('#darkModeToggle').on('click', function(e) {
e.preventDefault();
if ($('body').hasClass('dark-mode')) {
$('body').removeClass('dark-mode');
setCookie('darkMode', 'light', 365);
} else {
$('body').addClass('dark-mode');
setCookie('darkMode', 'dark', 365);
}
syncDarkModeUI();
});
});
</script>
{% include 'template_parts/base_messages.html' %}
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<nav class="main-header navbar navbar-expand {% if request.COOKIES.darkMode == 'dark' %}navbar-dark{% else %}navbar-white navbar-light{% endif %}">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
@@ -10,6 +10,11 @@
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a href="#" class="nav-link" id="darkModeToggle" title="{% trans 'Toggle Dark Mode' %}">
<i class="fas {% if request.COOKIES.darkMode == 'dark' %}fa-sun{% else %}fa-moon{% endif %}" id="darkModeIcon"></i>
</a>
</li>
<li class="nav-item">
<a href="/change_language/" class="nav-link" title="{% trans 'Change Language' %}">
<i class="fas fa-language"></i>

View File

@@ -104,6 +104,46 @@
</script>
<script>
function syncDarkModeUI() {
var isDark = $('body').hasClass('dark-mode');
if (isDark) {
$('.main-header').removeClass('navbar-white navbar-light').addClass('navbar-dark');
$('#darkModeIcon').removeClass('fa-moon').addClass('fa-sun');
} else {
$('.main-header').removeClass('navbar-dark').addClass('navbar-white navbar-light');
$('#darkModeIcon').removeClass('fa-sun').addClass('fa-moon');
}
}
$(document).ready(function() {
syncDarkModeUI();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!getCookie('darkMode')) {
if (e.matches) {
$('body').addClass('dark-mode');
} else {
$('body').removeClass('dark-mode');
}
syncDarkModeUI();
}
});
}
$('#darkModeToggle').on('click', function(e) {
e.preventDefault();
if ($('body').hasClass('dark-mode')) {
$('body').removeClass('dark-mode');
setCookie('darkMode', 'light', 365);
} else {
$('body').addClass('dark-mode');
setCookie('darkMode', 'dark', 365);
}
syncDarkModeUI();
});
});
$(document).ready(function() {
var sidebarState = getCookie('sidebarState');
if (sidebarState === 'collapsed') {

View File

@@ -1,31 +0,0 @@
<style>
.peer-extra-info {
display: none;
}
.callout.position-relative {
padding: 0 !important;
}
#inviteTextContainer {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
#inviteText {
white-space: pre-line;
text-align: left;
}
.div-peer-text-information {
top: 0;
left: 0;
background: linear-gradient(to right, white, transparent);
width: 100%;
height: 100%;
}
.peer-lock-icon {
position: absolute;
right: 15px;
top: 48px;
opacity: 0.75;
}
</style>

View File

@@ -95,7 +95,8 @@
$('#downloadConfigButton').attr('href', downloadUrl);
$('#qrcodeButton').attr('href', qrUrl);
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid).show();
var darkParam = $('body').hasClass('dark-mode') ? '&dark=1' : '';
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid + darkParam).show();
$('#peerPreviewModal').data('peer-uuid', uuid);
$.ajax({
@@ -130,7 +131,8 @@
e.preventDefault();
var period = $(this).data('period');
var uuid = $('#peerPreviewModal').data('peer-uuid');
var newSrc = '/rrd/graph/?peer=' + uuid + '&period=' + period;
var darkParam = $('body').hasClass('dark-mode') ? '&dark=1' : '';
var newSrc = '/rrd/graph/?peer=' + uuid + '&period=' + period + darkParam;
$('#graphImg').attr('src', newSrc);
});
</script>

View File

@@ -74,7 +74,7 @@
</div>
</div>
<div class="card-body">
<img src="/rrd/graph/?instance={{ instance.uuid }}&period={{ request.GET.period|default:'6h' }}"
<img src="/rrd/graph/?instance={{ instance.uuid }}&period={{ request.GET.period|default:'6h' }}{% if request.COOKIES.darkMode == 'dark' %}&dark=1{% endif %}"
class="img-fluid" alt="RRD Graph">
</div>
</div>

View File

@@ -148,7 +148,7 @@
</div>
<center>
<img id="graphImg" src="/rrd/graph/?peer={{ current_peer.uuid }}{% if request.GET.period %}&period={{ request.GET.period }}{% endif %}"
<img id="graphImg" src="/rrd/graph/?peer={{ current_peer.uuid }}{% if request.GET.period %}&period={{ request.GET.period }}{% endif %}{% if request.COOKIES.darkMode == 'dark' %}&dark=1{% endif %}"
class="img-fluid" alt="{% trans 'No traffic history, please wait a few minutes' %}"
onerror="this.onerror=null; this.style.display='none'; this.insertAdjacentHTML('afterend', this.alt);">
</center>
@@ -324,7 +324,8 @@
button.addEventListener('click', function (e) {
e.preventDefault();
var period = this.getAttribute('data-period');
var newSrc = '/rrd/graph/?peer={{ current_peer.uuid }}&period=' + period;
var darkParam = $('body').hasClass('dark-mode') ? '&dark=1' : '';
var newSrc = '/rrd/graph/?peer={{ current_peer.uuid }}&period=' + period + darkParam;
var imgElement = document.getElementById('graphImg');
if (imgElement) {
imgElement.setAttribute('src', newSrc);

View File

@@ -40,7 +40,7 @@
<div class="col-md-6">
<h3>wg{{ wireguard_instance.instance_id }} {% if wireguard_instance.name %}({{ wireguard_instance.name }}){% endif %}</h3>
<p><b><i class="fas fa-chart-area nav-icon"></i> {% trans 'Instance Traffic' %}</b></p>
<img id="graphImg" src="/rrd/graph/?instance={{ wireguard_instance.uuid }}{% if request.GET.period %}&period={{ request.GET.period }}{% endif %}" class="img-fluid" alt="No traffic history, please wait a few minutes" onerror="this.onerror=null; this.style.display='none'; this.insertAdjacentHTML('afterend', this.alt);">
<img id="graphImg" src="/rrd/graph/?instance={{ wireguard_instance.uuid }}{% if request.GET.period %}&period={{ request.GET.period }}{% endif %}{% if request.COOKIES.darkMode == 'dark' %}&dark=1{% endif %}" class="img-fluid" alt="No traffic history, please wait a few minutes" onerror="this.onerror=null; this.style.display='none'; this.insertAdjacentHTML('afterend', this.alt);">
<p>
<b><i class="fas fa-network-wired nav-icon"></i> {% trans 'IP Address' %}: </b>{{ wireguard_instance.address }}/{{ wireguard_instance.netmask }}<br>
<b><i class="fas fa-link nav-icon"></i> {% trans 'Public Address' %}: </b>{{ wireguard_instance.hostname }}<br>

View File

@@ -1,18 +1,17 @@
import base64
import os
import subprocess
import tempfile
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404
from django.shortcuts import get_object_or_404
from user_manager.models import UserAcl
from wgwadmlibrary.tools import user_has_access_to_peer
from wireguard.models import Peer, WireGuardInstance
import base64
import tempfile
import os
@login_required
def view_rrd_graph(request):
@@ -45,21 +44,44 @@ def view_rrd_graph(request):
if not (period[:-1].isdigit() and period[-1] in ['h', 'd']):
period = '6h'
dark_mode = request.GET.get('dark') == '1'
command = [
"rrdtool", "graph", graph_file,
"--start", f"-{period}",
"--title", f"{graph_title}",
"--vertical-label", "Value",
]
if dark_mode:
command += [
"--color", "BACK#2d3035",
"--color", "CANVAS#353a40",
"--color", "SHADEA#2d3035",
"--color", "SHADEB#2d3035",
"--color", "GRID#555c63",
"--color", "MGRID#6c7580",
"--color", "FONT#c8c8c8",
"--color", "FRAME#2d3035",
"--color", "ARROW#c8c8c8",
]
tx_color = "4a9eff"
rx_color = "ff6b6b"
else:
tx_color = "0000FF"
rx_color = "FF0000"
command += [
f"DEF:txdata={rrd_file_path}:tx:AVERAGE",
f"DEF:rxdata={rrd_file_path}:rx:AVERAGE",
"CDEF:tx_mb=txdata,1048576,/",
"CDEF:rx_mb=rxdata,1048576,/",
"VDEF:tx_total=tx_mb,TOTAL",
"VDEF:rx_total=rx_mb,TOTAL",
"LINE1:txdata#0000FF:Transmitted ",
f"LINE1:txdata#{tx_color}:Transmitted ",
"GPRINT:tx_total:%6.2lf MB",
"COMMENT:\\n",
"LINE1:rxdata#FF0000:Received ",
f"LINE1:rxdata#{rx_color}:Received ",
"GPRINT:rx_total:%6.2lf MB",
"COMMENT:\\n"
]