diff --git a/.gitattributes b/.gitattributes index fab27f433..53f57e659 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 \ No newline at end of file diff --git a/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor b/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor index 4b466c11d..28c8c40f7 100644 --- a/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor +++ b/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor @@ -45,6 +45,8 @@ /// protected override async Task OnAfterRenderAsync(bool firstRender) { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) { await RefreshData(); diff --git a/src/AliasVault.Admin/Main/Pages/MainBase.cs b/src/AliasVault.Admin/Main/Pages/MainBase.cs index a83febf47..c3abdc081 100644 --- a/src/AliasVault.Admin/Main/Pages/MainBase.cs +++ b/src/AliasVault.Admin/Main/Pages/MainBase.cs @@ -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; /// @@ -73,6 +73,12 @@ public abstract class MainBase : OwningComponentBase [Inject] protected ConfirmModalService ConfirmModalService { get; set; } = null!; + /// + /// Gets or sets the ApexChartService. + /// + [Inject] + protected IApexChartService ApexChartService { get; set; } = null!; + /// /// Gets or sets the injected JSRuntime instance. /// @@ -96,6 +102,18 @@ public abstract class MainBase : OwningComponentBase BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationService.BaseUri }); } + /// + 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(); + } + } + /// /// Gets the username from the authentication state asynchronously. /// @@ -104,4 +122,50 @@ public abstract class MainBase : OwningComponentBase { return UserService.User().UserName ?? "[Unknown]"; } + + /// + /// Sets the default ApexCharts chart color based on the dark mode setting. + /// + private async Task SetDefaultApexChartOptionsAsync() + { + var darkMode = await JsInvokeService.RetryInvokeWithResultAsync("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); + } } diff --git a/src/AliasVault.Admin/Program.cs b/src/AliasVault.Admin/Program.cs index c093a9240..243cc6ebe 100644 --- a/src/AliasVault.Admin/Program.cs +++ b/src/AliasVault.Admin/Program.cs @@ -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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot")); +builder.Services.AddApexCharts(); builder.Services.AddAuthentication(options => { diff --git a/src/AliasVault.Admin/Services/JSInvokeService.cs b/src/AliasVault.Admin/Services/JSInvokeService.cs index efc8d5646..6fc5e5bfb 100644 --- a/src/AliasVault.Admin/Services/JSInvokeService.cs +++ b/src/AliasVault.Admin/Services/JSInvokeService.cs @@ -50,4 +50,44 @@ public class JsInvokeService(IJSRuntime js) // Optionally log that the JS function could not be called after maxAttempts } + + /// + /// Invoke a JavaScript function with retry and exponential backoff that returns a value. + /// + /// The type of value to return from the JavaScript function. + /// The JS function name to call. + /// Initial delay before calling the function. + /// Maximum attempts before giving up. + /// Arguments to pass on to the javascript function. + /// The value returned from the JavaScript function. + /// Thrown when the JS function could not be called after all attempts. + public async Task RetryInvokeWithResultAsync(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("isFunctionDefined", functionName); + if (isDefined) + { + return await js.InvokeAsync(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."); + } } diff --git a/src/AliasVault.Admin/wwwroot/css/tailwind.css b/src/AliasVault.Admin/wwwroot/css/tailwind.css index 6ebbd6756..419872ae4 100644 --- a/src/AliasVault.Admin/wwwroot/css/tailwind.css +++ b/src/AliasVault.Admin/wwwroot/css/tailwind.css @@ -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; } diff --git a/src/AliasVault.Admin/wwwroot/js/utilities.js b/src/AliasVault.Admin/wwwroot/js/utilities.js index 3fc33831b..a6c44811d 100644 --- a/src/AliasVault.Admin/wwwroot/js/utilities.js +++ b/src/AliasVault.Admin/wwwroot/js/utilities.js @@ -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');