Add ApexChart service and integrate dark mode (#641)

This commit is contained in:
Leendert de Borst
2025-03-19 19:15:07 +01:00
parent 917d6f6bcc
commit 9ea845b497
7 changed files with 127 additions and 10 deletions

19
.gitattributes vendored
View File

@@ -1,12 +1,17 @@
# Set default behavior to automatically normalize line endings
* text=auto
# Shell scripts should always use LF (Unix-style) line endings
# Common files should always use LF (Unix-style) line endings
*.sh text eol=lf
# Batch scripts should always use CRLF (Windows-style) line endings
*.bat text eol=crlf
*.cmd text eol=crlf
*.cs text eol=lf
*.razor text eol=lf
*.css text eol=lf
*.html text eol=lf
*.js text eol=lf
*.json text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# Docker files should use LF
Dockerfile text eol=lf
@@ -17,6 +22,10 @@ docker-compose*.yml text eol=lf
*.config text eol=lf
.env* text eol=lf
# Batch scripts should always use CRLF (Windows-style) line endings
*.bat text eol=crlf
*.cmd text eol=crlf
# Documentation should be normalized
*.md text
*.txt text

View File

@@ -45,6 +45,8 @@
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await RefreshData();

View File

@@ -12,9 +12,9 @@ using AliasVault.Admin.Services;
using AliasVault.Auth;
using AliasVault.RazorComponents.Models;
using AliasVault.RazorComponents.Services;
using ApexCharts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using Microsoft.JSInterop;
/// <summary>
@@ -73,6 +73,12 @@ public abstract class MainBase : OwningComponentBase
[Inject]
protected ConfirmModalService ConfirmModalService { get; set; } = null!;
/// <summary>
/// Gets or sets the ApexChartService.
/// </summary>
[Inject]
protected IApexChartService ApexChartService { get; set; } = null!;
/// <summary>
/// Gets or sets the injected JSRuntime instance.
/// </summary>
@@ -96,6 +102,18 @@ public abstract class MainBase : OwningComponentBase
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationService.BaseUri });
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Update default ApexCharts chart color based on the dark mode setting.
await SetDefaultApexChartOptionsAsync();
}
}
/// <summary>
/// Gets the username from the authentication state asynchronously.
/// </summary>
@@ -104,4 +122,50 @@ public abstract class MainBase : OwningComponentBase
{
return UserService.User().UserName ?? "[Unknown]";
}
/// <summary>
/// Sets the default ApexCharts chart color based on the dark mode setting.
/// </summary>
private async Task SetDefaultApexChartOptionsAsync()
{
var darkMode = await JsInvokeService.RetryInvokeWithResultAsync<bool>("isDarkMode", TimeSpan.Zero, 5);
var options = new ApexChartBaseOptions
{
Chart = new Chart
{
ForeColor = darkMode ? "#bbb" : "#555",
},
Fill = new Fill
{
Colors = darkMode ?
[
"#FFB84D", // Bright gold
"#8B6CB9", // Darker Purple
"#68A890", // Darker Sea Green
"#CD5C5C", // Darker Coral
"#4F94CD", // Darker Sky Blue
"#BA55D3", // Darker Plum
"#CDC673", // Darker Khaki
"#6B8E23", // Darker Sage Green
"#CD853F", // Darker Burlywood
"#7B68EE", // Darker Slate Blue
]
:
[
"#FFB366", // Light Orange
"#B19CD9", // Light Purple
"#98D8C1", // Light Sea Green
"#F08080", // Light Coral
"#87CEEB", // Sky Blue
"#DDA0DD", // Plum
"#F0E68C", // Khaki
"#9CB071", // Sage Green
"#DEB887", // Burlywood
"#A7A1E8", // Light Slate Blue
],
},
};
await ApexChartService.SetGlobalOptionsAsync(options, false);
}
}

View File

@@ -19,6 +19,7 @@ using AliasVault.Logging;
using AliasVault.RazorComponents.Services;
using AliasVault.Shared.Models.Configuration;
using AliasVault.Shared.Server.Services;
using ApexCharts;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
@@ -61,6 +62,7 @@ builder.Services.AddScoped<AuthLoggingService>();
builder.Services.AddScoped<ConfirmModalService>();
builder.Services.AddScoped<ServerSettingsService>();
builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot"));
builder.Services.AddApexCharts();
builder.Services.AddAuthentication(options =>
{

View File

@@ -50,4 +50,44 @@ public class JsInvokeService(IJSRuntime js)
// Optionally log that the JS function could not be called after maxAttempts
}
/// <summary>
/// Invoke a JavaScript function with retry and exponential backoff that returns a value.
/// </summary>
/// <typeparam name="TValue">The type of value to return from the JavaScript function.</typeparam>
/// <param name="functionName">The JS function name to call.</param>
/// <param name="initialDelay">Initial delay before calling the function.</param>
/// <param name="maxAttempts">Maximum attempts before giving up.</param>
/// <param name="args">Arguments to pass on to the javascript function.</param>
/// <returns>The value returned from the JavaScript function.</returns>
/// <exception cref="InvalidOperationException">Thrown when the JS function could not be called after all attempts.</exception>
public async Task<TValue> RetryInvokeWithResultAsync<TValue>(string functionName, TimeSpan initialDelay, int maxAttempts, params object[] args)
{
TimeSpan delay = initialDelay;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
bool isDefined = await js.InvokeAsync<bool>("isFunctionDefined", functionName);
if (isDefined)
{
return await js.InvokeAsync<TValue>(functionName, args);
}
}
catch
{
// Optionally log the exception
}
// Wait for the delay before the next attempt
await Task.Delay(delay);
// Exponential backoff: double the delay for the next attempt
delay = TimeSpan.FromTicks(delay.Ticks * 2);
}
// All attempts failed, throw an exception
throw new InvalidOperationException($"Failed to invoke JavaScript function '{functionName}' after {maxAttempts} attempts.");
}
}

View File

@@ -1969,10 +1969,6 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900\/30:is(.dark *) {
background-color: rgb(113 63 18 / 0.3);
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}

View File

@@ -14,6 +14,10 @@ window.initTopMenu = function() {
initDarkModeSwitcher();
};
window.isDarkMode = function() {
return document.documentElement.classList.contains('dark');
};
window.registerClickOutsideHandler = (dotNetHelper) => {
document.addEventListener('click', (event) => {
const menu = document.getElementById('userMenuDropdown');