diff --git a/KAST.Core/Services/ThemeService.cs b/KAST.Core/Services/ThemeService.cs new file mode 100644 index 0000000..75a8d37 --- /dev/null +++ b/KAST.Core/Services/ThemeService.cs @@ -0,0 +1,64 @@ +using KAST.Data.Models; + +namespace KAST.Core.Services +{ + public class ThemeService + { + private readonly ConfigService _configService; + private bool _isDarkMode = false; + + public event Action? OnThemeChanged; + + public ThemeService(ConfigService configService) + { + _configService = configService; + } + + public bool IsDarkMode => _isDarkMode; + + public async Task InitializeAsync() + { + var config = await _configService.GetConfigAsync(); + await UpdateThemeFromSettings(config.ThemeMode); + } + + public async Task SetThemeModeAsync(string themeMode) + { + var config = await _configService.GetConfigAsync(); + config.ThemeMode = themeMode; + await _configService.UpdateConfigAsync(config); + await UpdateThemeFromSettings(themeMode); + } + + public async Task ToggleDarkModeAsync() + { + var newMode = _isDarkMode ? "light" : "dark"; + await SetThemeModeAsync(newMode); + } + + private async Task UpdateThemeFromSettings(string? themeMode) + { + bool newDarkMode = themeMode switch + { + "dark" => true, + "light" => false, + "auto" => await GetSystemPreferenceAsync(), + _ => false + }; + + if (_isDarkMode != newDarkMode) + { + _isDarkMode = newDarkMode; + OnThemeChanged?.Invoke(_isDarkMode); + } + } + + private async Task GetSystemPreferenceAsync() + { + // For now, default to light mode for "auto" + // In a real implementation, this could detect system preference + await Task.CompletedTask; + return false; + } + } +} \ No newline at end of file diff --git a/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.Designer.cs b/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.Designer.cs new file mode 100644 index 0000000..943710e --- /dev/null +++ b/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.Designer.cs @@ -0,0 +1,82 @@ +// +using System; +using KAST.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KAST.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250926081639_EnhanceSettingsModel")] + partial class EnhanceSettingsModel + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("KAST.Data.Models.KastSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("CheckForUpdates") + .HasColumnType("INTEGER"); + + b.Property("DebugLogging") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("ModFolderPath") + .HasColumnType("TEXT"); + + b.Property("ServerDefaultPath") + .HasColumnType("TEXT"); + + b.Property("ThemeAccent") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("ThemeMode") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KAST.Data.Models.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("InstallPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Servers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.cs b/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.cs new file mode 100644 index 0000000..f134292 --- /dev/null +++ b/KAST.Data/Migrations/20250926081639_EnhanceSettingsModel.cs @@ -0,0 +1,236 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KAST.Data.Migrations +{ + /// + public partial class EnhanceSettingsModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ModProfiles"); + + migrationBuilder.DropTable( + name: "ProfileHistories"); + + migrationBuilder.DropTable( + name: "Mods"); + + migrationBuilder.DropTable( + name: "Profiles"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Servers"); + + migrationBuilder.DropColumn( + name: "LastUpdated", + table: "Servers"); + + migrationBuilder.DropColumn( + name: "Version", + table: "Servers"); + + migrationBuilder.AddColumn( + name: "CheckForUpdates", + table: "Settings", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "DebugLogging", + table: "Settings", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "Language", + table: "Settings", + type: "TEXT", + maxLength: 10, + nullable: true); + + migrationBuilder.AddColumn( + name: "ThemeMode", + table: "Settings", + type: "TEXT", + maxLength: 10, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CheckForUpdates", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "DebugLogging", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "Language", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "ThemeMode", + table: "Settings"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Servers", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastUpdated", + table: "Servers", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "Version", + table: "Servers", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "Mods", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Author = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + IsLocal = table.Column(type: "INTEGER", nullable: false), + LastUpdated = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", nullable: false), + SizeBytes = table.Column(type: "INTEGER", nullable: false), + SteamId = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Mods", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Profiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ServerId = table.Column(type: "TEXT", nullable: true), + CommandLineArgs = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + LastModified = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 255, nullable: false), + PerformanceConfig = table.Column(type: "TEXT", nullable: true), + ServerConfig = table.Column(type: "TEXT", nullable: true), + ServerProfile = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Profiles", x => x.Id); + table.ForeignKey( + name: "FK_Profiles_Servers_ServerId", + column: x => x.ServerId, + principalTable: "Servers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ModProfiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ModId = table.Column(type: "TEXT", nullable: false), + ProfileId = table.Column(type: "TEXT", nullable: false), + AddedAt = table.Column(type: "TEXT", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + Order = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ModProfiles", x => x.Id); + table.ForeignKey( + name: "FK_ModProfiles_Mods_ModId", + column: x => x.ModId, + principalTable: "Mods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ModProfiles_Profiles_ProfileId", + column: x => x.ProfileId, + principalTable: "Profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProfileHistories", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ProfileId = table.Column(type: "TEXT", nullable: false), + ChangeDescription = table.Column(type: "TEXT", nullable: true), + ChangedBy = table.Column(type: "TEXT", nullable: true), + CommandLineArgsSnapshot = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ModsSnapshot = table.Column(type: "TEXT", nullable: true), + PerformanceConfigSnapshot = table.Column(type: "TEXT", nullable: true), + ServerConfigSnapshot = table.Column(type: "TEXT", nullable: true), + ServerProfileSnapshot = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProfileHistories", x => x.Id); + table.ForeignKey( + name: "FK_ProfileHistories_Profiles_ProfileId", + column: x => x.ProfileId, + principalTable: "Profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ModProfiles_ModId_ProfileId", + table: "ModProfiles", + columns: new[] { "ModId", "ProfileId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ModProfiles_ProfileId", + table: "ModProfiles", + column: "ProfileId"); + + migrationBuilder.CreateIndex( + name: "IX_Mods_SteamId", + table: "Mods", + column: "SteamId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProfileHistories_ProfileId_Version", + table: "ProfileHistories", + columns: new[] { "ProfileId", "Version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Profiles_ServerId", + table: "Profiles", + column: "ServerId"); + } + } +} diff --git a/KAST.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/KAST.Data/Migrations/ApplicationDbContextModelSnapshot.cs index b6d9818..829057f 100644 --- a/KAST.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/KAST.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -15,45 +15,63 @@ partial class ApplicationDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.12"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); - modelBuilder.Entity("KAST.Data.Models.Server", b => + modelBuilder.Entity("KAST.Data.Models.KastSettings", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("InstallPath") - .IsRequired() + b.Property("ApiKey") .HasColumnType("TEXT"); - b.Property("Name") - .IsRequired() + b.Property("CheckForUpdates") + .HasColumnType("INTEGER"); + + b.Property("DebugLogging") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("ModFolderPath") + .HasColumnType("TEXT"); + + b.Property("ServerDefaultPath") + .HasColumnType("TEXT"); + + b.Property("ThemeAccent") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("ThemeMode") + .HasMaxLength(10) .HasColumnType("TEXT"); b.HasKey("Id"); - b.ToTable("Servers"); + b.ToTable("Settings"); }); - modelBuilder.Entity("KAST.Data.Models.Settings", b => + modelBuilder.Entity("KAST.Data.Models.Server", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("ModFolderPath") + b.Property("InstallPath") .IsRequired() .HasColumnType("TEXT"); - b.Property("ThemeAccent") + b.Property("Name") .IsRequired() - .HasMaxLength(10) .HasColumnType("TEXT"); b.HasKey("Id"); - b.ToTable("Settings"); + b.ToTable("Servers"); }); #pragma warning restore 612, 618 } diff --git a/KAST.Data/Models/KastSettings.cs b/KAST.Data/Models/KastSettings.cs index 64946a0..5076a29 100644 --- a/KAST.Data/Models/KastSettings.cs +++ b/KAST.Data/Models/KastSettings.cs @@ -27,10 +27,36 @@ public class KastSettings [EnvVariableAttribute("KAST_STEAM_API_KEY")] public string? ApiKey { get; set; } - /// - /// Gets or sets the default Arma server path. + /// + /// Gets or sets the default Arma server path. /// [EnvVariableAttribute("KAST_SERVER_DEFAULT_PATH")] public string? ServerDefaultPath { get; set; } + + /// + /// Gets or sets the theme mode (light, dark, auto). + /// + [MaxLength(10)] + [EnvVariableAttribute("KAST_THEME_MODE")] + public string? ThemeMode { get; set; } = "auto"; + + /// + /// Gets or sets whether to check for updates on startup. + /// + [EnvVariableAttribute("KAST_CHECK_UPDATES")] + public bool? CheckForUpdates { get; set; } = true; + + /// + /// Gets or sets the UI language/locale. + /// + [MaxLength(10)] + [EnvVariableAttribute("KAST_LANGUAGE")] + public string? Language { get; set; } = "en"; + + /// + /// Gets or sets whether to enable debug logging. + /// + [EnvVariableAttribute("KAST_DEBUG_LOGGING")] + public bool? DebugLogging { get; set; } = false; } } diff --git a/KAST/Components/Layout/MainLayout.razor b/KAST/Components/Layout/MainLayout.razor index ef85570..c81c9aa 100644 --- a/KAST/Components/Layout/MainLayout.razor +++ b/KAST/Components/Layout/MainLayout.razor @@ -1,4 +1,7 @@ @inherits LayoutComponentBase +@using KAST.Core.Services +@inject ThemeService ThemeService +@implements IDisposable @@ -36,7 +39,7 @@ private bool _isDarkMode = false; private MudTheme? _theme = null; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { _theme = new() { @@ -44,6 +47,13 @@ PaletteDark = _darkPalette, LayoutProperties = new LayoutProperties() { DefaultBorderRadius = "0" } }; + + // Initialize theme service and sync with its state + await ThemeService.InitializeAsync(); + _isDarkMode = ThemeService.IsDarkMode; + + // Subscribe to theme changes + ThemeService.OnThemeChanged += OnThemeChanged; } private void DrawerToggle() @@ -51,9 +61,20 @@ _drawerOpen = !_drawerOpen; } - private void DarkModeToggle() + private async Task DarkModeToggle() + { + await ThemeService.ToggleDarkModeAsync(); + } + + private void OnThemeChanged(bool isDarkMode) + { + _isDarkMode = isDarkMode; + InvokeAsync(StateHasChanged); + } + + public void Dispose() { - _isDarkMode = !_isDarkMode; + ThemeService.OnThemeChanged -= OnThemeChanged; } private readonly PaletteLight _lightPalette = new() {}; diff --git a/KAST/Components/Pages/Settings.razor b/KAST/Components/Pages/Settings.razor index 077e422..5b60793 100644 --- a/KAST/Components/Pages/Settings.razor +++ b/KAST/Components/Pages/Settings.razor @@ -4,16 +4,18 @@ @using KAST.Data.Models @using KAST.Core.Extensions @inject ConfigService ConfigService +@inject ThemeService ThemeService @inject ISnackbar Snackbar @inject IDialogService DialogService Settings Settings - +
@@ -42,13 +44,49 @@
- - Save - - +
+ + + Auto (System) + Light + Dark + +
+ +
+ + + English + Français + Deutsch + Español + +
+ +
+ +
+ +
+ +
+
@code { private KastSettings config = new(); @@ -58,10 +96,49 @@ config = await ConfigService.GetConfigAsync(); } - private async Task SaveConfig() + private async Task SaveConfigAsync() + { + try + { + await ConfigService.UpdateConfigAsync(config); + Snackbar.Add("Settings saved", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to save settings: {ex.Message}", Severity.Error); + } + } + + private async Task OnApiKeyChanged() + { + await SaveConfigAsync(); + } + + private async Task OnThemeAccentChanged() + { + await SaveConfigAsync(); + } + + private async Task OnThemeModeChanged() + { + // Update the theme service when theme mode changes + await ThemeService.SetThemeModeAsync(config.ThemeMode ?? "auto"); + Snackbar.Add("Theme updated", Severity.Success); + } + + private async Task OnLanguageChanged() + { + await SaveConfigAsync(); + } + + private async Task OnCheckForUpdatesChanged() + { + await SaveConfigAsync(); + } + + private async Task OnDebugLoggingChanged() { - await ConfigService.UpdateConfigAsync(config); - Snackbar.Add("Settings saved", Severity.Success); + await SaveConfigAsync(); } private async Task PickFolder(string propertyName) @@ -73,7 +150,10 @@ { var prop = typeof(KastSettings).GetProperty(propertyName); if (prop != null && prop.CanWrite) + { prop.SetValue(config, folderPath); + await SaveConfigAsync(); + } } } } diff --git a/KAST/KAST.csproj b/KAST/KAST.csproj index 763155e..636b6b0 100644 --- a/KAST/KAST.csproj +++ b/KAST/KAST.csproj @@ -7,6 +7,10 @@ 930b9a04-f06a-4052-9bae-75bf7da13f28 + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/KAST/Program.cs b/KAST/Program.cs index c576d64..68e719c 100644 --- a/KAST/Program.cs +++ b/KAST/Program.cs @@ -2,7 +2,7 @@ using KAST.Core.Helpers; using KAST.Core.Services; using KAST.Data; -using KAST.Data.Interfaces; +using KAST.Core.Interfaces; using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using System.Diagnostics; @@ -30,11 +30,12 @@ public static async Task Main(string[] args) builder.Services.AddSingleton(new ActivitySource(builder.Environment.ApplicationName)); builder.Services.AddSingleton(); builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(sp => - { - var env = sp.GetRequiredService(); // Get environment - return new FileSystemService(env.ContentRootPath); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(sp => + { + var env = sp.GetRequiredService(); // Get environment + return new FileSystemService(env.ContentRootPath); }); var app = builder.Build();