refactor(app): add theme toggle functionality and improve theme management
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
<div class="dialog-content" @onclick:stopPropagation="true">
|
<div class="dialog-content" @onclick:stopPropagation="true">
|
||||||
<h5>Über Dümpelkas · Version @AppVersion</h5>
|
<h5>Über Dümpelkas · Version @AppVersion</h5>
|
||||||
<p class="mb-2">Entwickler: Andre Beging</p>
|
<p class="mb-2">Entwickler: Andre Beging</p>
|
||||||
<p class="mb-3">E-Mail: <a href="mailto:mail@beging.de" style="color: white;">mail@beging.de</a></p>
|
<p class="mb-3">E-Mail: <a href="mailto:mail@beging.de" style="color: var(--color-text);">mail@beging.de</a></p>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAboutDialog">
|
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAboutDialog">
|
||||||
<i class="bi bi-x-lg"></i> Schließen
|
<i class="bi bi-x-lg"></i> Schließen
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
||||||
<i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
|
<i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-nav btn-secondary" @onclick="ToggleThemeAsync" title="Farbmodus wechseln">
|
||||||
|
<i class="@ThemeButtonIconClass"></i> Modus<br>@ThemeButtonLabel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (accounts != null && accounts.Any())
|
@if (accounts != null && accounts.Any())
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Duempelkas.App.Services.Models;
|
using Duempelkas.App.Services.Models;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace Duempelkas.App.Pages;
|
namespace Duempelkas.App.Pages;
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ public partial class Dashboard
|
|||||||
private string? operationMessage;
|
private string? operationMessage;
|
||||||
private string operationMessageClass = "alert-info";
|
private string operationMessageClass = "alert-info";
|
||||||
private string? savedPdfPath;
|
private string? savedPdfPath;
|
||||||
|
private string currentTheme = "light";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -37,6 +40,26 @@ public partial class Dashboard
|
|||||||
await LoadAll();
|
await LoadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentTheme = await JS.InvokeAsync<string>("duempelkasThemeGetCurrentTheme");
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
// Keep default light mode when JS interop is not ready or unavailable.
|
||||||
|
currentTheme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Data Loading
|
#region Data Loading
|
||||||
@@ -167,5 +190,24 @@ public partial class Dashboard
|
|||||||
|
|
||||||
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
|
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
|
||||||
|
|
||||||
|
private string ThemeButtonLabel => currentTheme == "dark" ? "Hell" : "Dunkel";
|
||||||
|
|
||||||
|
private string ThemeButtonIconClass => currentTheme == "dark" ? "bi bi-sun" : "bi bi-moon-stars";
|
||||||
|
|
||||||
|
private async Task ToggleThemeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentTheme = await JS.InvokeAsync<string>("duempelkasThemeToggleTheme");
|
||||||
|
}
|
||||||
|
catch (JSException)
|
||||||
|
{
|
||||||
|
currentTheme = currentTheme == "dark" ? "light" : "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IJSRuntime JS { get; set; } = default!;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
/* Duempelkas App Styles – Dark Mode */
|
/* Duempelkas App Styles */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-income: #4ade80;
|
--color-income: #4ade80;
|
||||||
--color-expense: #f87171;
|
--color-expense: #f87171;
|
||||||
--color-transfer: #60a5fa;
|
--color-transfer: #60a5fa;
|
||||||
|
--color-bg: #ddd;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-hover: #f1f5f9;
|
||||||
|
--color-border: #d9e2ef;
|
||||||
|
--color-text: #0f172a;
|
||||||
|
--color-text-muted: #64748b;
|
||||||
|
--color-accent: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-ui-theme="dark"] {
|
||||||
--color-bg: #1a1a2e;
|
--color-bg: #1a1a2e;
|
||||||
--color-surface: #16213e;
|
--color-surface: #16213e;
|
||||||
--color-surface-hover: #1e2d4a;
|
--color-surface-hover: #1e2d4a;
|
||||||
@@ -311,7 +321,7 @@ html, body {
|
|||||||
.btn-dark {
|
.btn-dark {
|
||||||
background-color: #334155;
|
background-color: #334155;
|
||||||
border-color: #475569;
|
border-color: #475569;
|
||||||
color: var(--color-text);
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-dark:hover {
|
.btn-dark:hover {
|
||||||
@@ -319,6 +329,12 @@ html, body {
|
|||||||
border-color: #64748b;
|
border-color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-ui-theme="light"] .btn-dark {
|
||||||
|
background-color: #334155;
|
||||||
|
border-color: #334155;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
/* Table */
|
/* Table */
|
||||||
.table {
|
.table {
|
||||||
--bs-table-bg: transparent;
|
--bs-table-bg: transparent;
|
||||||
|
|||||||
@@ -1,9 +1,113 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de" data-bs-theme="dark">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function getCookie(name) {
|
||||||
|
var parts = document.cookie ? document.cookie.split(';') : [];
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var part = parts[i].trim();
|
||||||
|
if (part.indexOf(name + '=') === 0) {
|
||||||
|
return decodeURIComponent(part.substring(name.length + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value) {
|
||||||
|
document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; max-age=31536000; samesite=lax';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredTheme() {
|
||||||
|
try {
|
||||||
|
var fromStorage = localStorage.getItem('duempelkas-theme');
|
||||||
|
if (fromStorage === 'light' || fromStorage === 'dark') {
|
||||||
|
return fromStorage;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore storage access issues and fall back to cookies/system.
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromCookie = getCookie('duempelkas-theme');
|
||||||
|
if (fromCookie === 'light' || fromCookie === 'dark') {
|
||||||
|
return fromCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSystemTheme() {
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTheme(value) {
|
||||||
|
return value === 'dark' ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme, persist) {
|
||||||
|
var resolved = normalizeTheme(theme);
|
||||||
|
document.documentElement.setAttribute('data-ui-theme', resolved);
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', resolved);
|
||||||
|
if (document.body) {
|
||||||
|
document.body.setAttribute('data-ui-theme', resolved);
|
||||||
|
document.body.setAttribute('data-bs-theme', resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('duempelkas-theme', resolved);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore storage access issues and keep cookie as fallback.
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie('duempelkas-theme', resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialTheme = getStoredTheme() || detectSystemTheme() || 'light';
|
||||||
|
applyTheme(initialTheme, false);
|
||||||
|
|
||||||
|
var duempelkasThemeGetCurrentTheme = function () {
|
||||||
|
var current = document.documentElement.getAttribute('data-ui-theme');
|
||||||
|
return normalizeTheme(current);
|
||||||
|
};
|
||||||
|
|
||||||
|
var duempelkasThemeSetTheme = function (theme) {
|
||||||
|
return applyTheme(theme, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
var duempelkasThemeToggleTheme = function () {
|
||||||
|
var current = document.documentElement.getAttribute('data-ui-theme');
|
||||||
|
var next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
return applyTheme(next, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.duempelkasThemeGetCurrentTheme = duempelkasThemeGetCurrentTheme;
|
||||||
|
globalThis.duempelkasThemeSetTheme = duempelkasThemeSetTheme;
|
||||||
|
globalThis.duempelkasThemeToggleTheme = duempelkasThemeToggleTheme;
|
||||||
|
|
||||||
|
window.duempelkasThemeGetCurrentTheme = duempelkasThemeGetCurrentTheme;
|
||||||
|
window.duempelkasThemeSetTheme = duempelkasThemeSetTheme;
|
||||||
|
window.duempelkasThemeToggleTheme = duempelkasThemeToggleTheme;
|
||||||
|
|
||||||
|
// Keep object-style API for compatibility with older calls.
|
||||||
|
window.duempelkasTheme = {
|
||||||
|
getCurrentTheme: duempelkasThemeGetCurrentTheme,
|
||||||
|
setTheme: duempelkasThemeSetTheme,
|
||||||
|
toggleTheme: duempelkasThemeToggleTheme
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<title>Dümpelkas – Kassenbuch</title>
|
<title>Dümpelkas – Kassenbuch</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|||||||
Reference in New Issue
Block a user