diff --git a/LANCommander.Launcher.Data/DatabaseContext.cs b/LANCommander.Launcher.Data/DatabaseContext.cs index a3938717..9e7c3d06 100644 --- a/LANCommander.Launcher.Data/DatabaseContext.cs +++ b/LANCommander.Launcher.Data/DatabaseContext.cs @@ -149,5 +149,6 @@ namespace LANCommander.Launcher.Data public DbSet? Media { get; set; } public DbSet? Libraries { get; set; } + public DbSet? Users { get; set; } } } \ No newline at end of file diff --git a/LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.Designer.cs b/LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.Designer.cs similarity index 98% rename from LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.Designer.cs rename to LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.Designer.cs index f8e76efb..c622340d 100644 --- a/LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.Designer.cs +++ b/LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace LANCommander.Launcher.Data.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20250119055819_AddLibraries")] - partial class AddLibraries + [Migration("20250119103515_AddUserLibraries")] + partial class AddUserLibraries { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -532,10 +532,9 @@ namespace LANCommander.Launcher.Data.Migrations .HasColumnType("TEXT"); b.Property("Alias") - .IsRequired() .HasColumnType("TEXT"); - b.Property("AvatarId") + b.Property("AvatarId") .HasColumnType("TEXT"); b.Property("CreatedOn") @@ -545,14 +544,13 @@ namespace LANCommander.Launcher.Data.Migrations .HasColumnType("TEXT"); b.Property("UserName") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); b.HasIndex("AvatarId"); - b.ToTable("User"); + b.ToTable("Users"); }); modelBuilder.Entity("LibraryGame", b => @@ -760,9 +758,7 @@ namespace LANCommander.Launcher.Data.Migrations { b.HasOne("LANCommander.Launcher.Data.Models.Media", "Avatar") .WithMany() - .HasForeignKey("AvatarId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("AvatarId"); b.Navigation("Avatar"); }); @@ -805,8 +801,7 @@ namespace LANCommander.Launcher.Data.Migrations modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b => { - b.Navigation("Library") - .IsRequired(); + b.Navigation("Library"); }); #pragma warning restore 612, 618 } diff --git a/LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.cs b/LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.cs similarity index 86% rename from LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.cs rename to LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.cs index 397e9714..7718c225 100644 --- a/LANCommander.Launcher.Data/Migrations/20250119055819_AddLibraries.cs +++ b/LANCommander.Launcher.Data/Migrations/20250119103515_AddUserLibraries.cs @@ -6,31 +6,30 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace LANCommander.Launcher.Data.Migrations { /// - public partial class AddLibraries : Migration + public partial class AddUserLibraries : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "User", + name: "Users", columns: table => new { Id = table.Column(type: "TEXT", nullable: false), - UserName = table.Column(type: "TEXT", nullable: false), - Alias = table.Column(type: "TEXT", nullable: false), - AvatarId = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", nullable: true), + Alias = table.Column(type: "TEXT", nullable: true), + AvatarId = table.Column(type: "TEXT", nullable: true), CreatedOn = table.Column(type: "TEXT", nullable: false), UpdatedOn = table.Column(type: "TEXT", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_User", x => x.Id); + table.PrimaryKey("PK_Users", x => x.Id); table.ForeignKey( - name: "FK_User_Media_AvatarId", + name: "FK_Users_Media_AvatarId", column: x => x.AvatarId, principalTable: "Media", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + principalColumn: "Id"); }); migrationBuilder.CreateTable( @@ -46,9 +45,9 @@ namespace LANCommander.Launcher.Data.Migrations { table.PrimaryKey("PK_Libraries", x => x.Id); table.ForeignKey( - name: "FK_Libraries_User_UserId", + name: "FK_Libraries_Users_UserId", column: x => x.UserId, - principalTable: "User", + principalTable: "Users", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -89,8 +88,8 @@ namespace LANCommander.Launcher.Data.Migrations column: "LibraryId"); migrationBuilder.CreateIndex( - name: "IX_User_AvatarId", - table: "User", + name: "IX_Users_AvatarId", + table: "Users", column: "AvatarId"); } @@ -104,7 +103,7 @@ namespace LANCommander.Launcher.Data.Migrations name: "Libraries"); migrationBuilder.DropTable( - name: "User"); + name: "Users"); } } } diff --git a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs index db21e414..a9166022 100644 --- a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -529,10 +529,9 @@ namespace LANCommander.Launcher.Data.Migrations .HasColumnType("TEXT"); b.Property("Alias") - .IsRequired() .HasColumnType("TEXT"); - b.Property("AvatarId") + b.Property("AvatarId") .HasColumnType("TEXT"); b.Property("CreatedOn") @@ -542,14 +541,13 @@ namespace LANCommander.Launcher.Data.Migrations .HasColumnType("TEXT"); b.Property("UserName") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); b.HasIndex("AvatarId"); - b.ToTable("User"); + b.ToTable("Users"); }); modelBuilder.Entity("LibraryGame", b => @@ -757,9 +755,7 @@ namespace LANCommander.Launcher.Data.Migrations { b.HasOne("LANCommander.Launcher.Data.Models.Media", "Avatar") .WithMany() - .HasForeignKey("AvatarId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("AvatarId"); b.Navigation("Avatar"); }); @@ -802,8 +798,7 @@ namespace LANCommander.Launcher.Data.Migrations modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b => { - b.Navigation("Library") - .IsRequired(); + b.Navigation("Library"); }); #pragma warning restore 612, 618 } diff --git a/LANCommander.Launcher.Data/Models/User.cs b/LANCommander.Launcher.Data/Models/User.cs index c10311e6..29589def 100644 --- a/LANCommander.Launcher.Data/Models/User.cs +++ b/LANCommander.Launcher.Data/Models/User.cs @@ -1,9 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace LANCommander.Launcher.Data.Models; +[Table("Users")] public class User : BaseModel { - public string UserName { get; set; } - public string Alias { get; set; } - public Media Avatar { get; set; } - public Library Library { get; set; } + public string? UserName { get; set; } + public string? Alias { get; set; } + public Media? Avatar { get; set; } + public Library? Library { get; set; } } \ No newline at end of file diff --git a/LANCommander.Launcher.Models/ConnectionState.cs b/LANCommander.Launcher.Models/ConnectionState.cs index 34b0fdf0..89ab6621 100644 --- a/LANCommander.Launcher.Models/ConnectionState.cs +++ b/LANCommander.Launcher.Models/ConnectionState.cs @@ -2,6 +2,7 @@ namespace LANCommander.Launcher.Models; public class ConnectionState { + public bool ValidCredentials { get; set; } public bool IsConnected { get; set; } public bool OfflineModeEnabled { get; set; } } \ No newline at end of file diff --git a/LANCommander.Launcher.Models/Settings.cs b/LANCommander.Launcher.Models/Settings.cs index 091fa8fa..29d7d70e 100644 --- a/LANCommander.Launcher.Models/Settings.cs +++ b/LANCommander.Launcher.Models/Settings.cs @@ -13,7 +13,6 @@ namespace LANCommander.Launcher.Models public AuthenticationSettings Authentication { get; set; } = new AuthenticationSettings(); public GameSettings Games { get; set; } = new GameSettings(); public MediaSettings Media { get; set; } = new MediaSettings(); - public ProfileSettings Profile { get; set; } = new ProfileSettings(); public FilterSettings Filter { get; set; } = new FilterSettings(); public DebugSettings Debug { get; set; } = new DebugSettings(); public UpdateSettings Updates { get; set; } = new UpdateSettings(); @@ -46,13 +45,6 @@ namespace LANCommander.Launcher.Models public string StoragePath { get; set; } = "Media"; } - public class ProfileSettings - { - public Guid Id { get; set; } - public string Alias { get; set; } - public Guid AvatarId { get; set; } - } - public class FilterSettings { public string? Title { get; set; } diff --git a/LANCommander.Launcher.Services/AuthenticationService.cs b/LANCommander.Launcher.Services/AuthenticationService.cs index 9cc19a79..588e7223 100644 --- a/LANCommander.Launcher.Services/AuthenticationService.cs +++ b/LANCommander.Launcher.Services/AuthenticationService.cs @@ -9,8 +9,6 @@ namespace LANCommander.Launcher.Services; public class AuthenticationService : BaseService { - private readonly ProfileService ProfileService; - private Settings Settings; public event EventHandler OnLogin; @@ -22,11 +20,9 @@ public class AuthenticationService : BaseService public AuthenticationService( Client client, - ILogger logger, - ProfileService profileService) : base(client, logger) + ILogger logger) : base(client, logger) { Settings = SettingService.GetSettings(); - ProfileService = profileService; } public bool IsConnected() @@ -83,8 +79,6 @@ public class AuthenticationService : BaseService SettingService.SaveSettings(Settings); OnLogin?.Invoke(this, EventArgs.Empty); - - await ProfileService.DownloadProfileInfoAsync(); } } @@ -117,8 +111,16 @@ public class AuthenticationService : BaseService SettingService.SaveSettings(Settings); OnRegister?.Invoke(this, EventArgs.Empty); - - await ProfileService.DownloadProfileInfoAsync(); + } + + public async Task ValidateConnectionAsync() + { + return await Client.ValidateTokenAsync(); + } + + public bool OfflineModeEnabled() + { + return Settings.Authentication.OfflineMode; } public void SetOfflineMode(bool state) @@ -140,8 +142,7 @@ public class AuthenticationService : BaseService await Client.LogoutAsync(); Settings = SettingService.GetSettings(); - - Settings.Profile = new ProfileSettings(); + Settings.Authentication = new AuthenticationSettings(); SettingService.SaveSettings(Settings); @@ -156,12 +157,24 @@ public class AuthenticationService : BaseService if (decodedToken == null) return Guid.Empty; - if (Guid.TryParse(decodedToken.Id, out Guid id)) + var claim = decodedToken.Claims.First(c => c.Type == ClaimTypes.NameIdentifier); + + if (Guid.TryParse(claim.Value, out Guid id)) return id; return Guid.Empty; } + public string GetCurrentUserName() + { + var decodedToken = DecodeToken(); + + if (decodedToken == null) + return String.Empty; + + return decodedToken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value; + } + public JwtSecurityToken DecodeToken() { if (Settings.Authentication.AccessToken == null) diff --git a/LANCommander.Launcher.Services/CommandLineService.cs b/LANCommander.Launcher.Services/CommandLineService.cs index 25b3277e..e8733827 100644 --- a/LANCommander.Launcher.Services/CommandLineService.cs +++ b/LANCommander.Launcher.Services/CommandLineService.cs @@ -12,6 +12,8 @@ namespace LANCommander.Launcher.Services { public class CommandLineService : BaseService { + private readonly AuthenticationService AuthenticationService; + private readonly UserService UserService; private readonly GameService GameService; private readonly InstallService InstallService; private readonly ImportService ImportService; @@ -22,11 +24,15 @@ namespace LANCommander.Launcher.Services public CommandLineService( SDK.Client client, ILogger logger, + AuthenticationService authenticationService, + UserService userService, GameService gameService, InstallService installService, ImportService importService, ProfileService profileService) : base(client, logger) { + AuthenticationService = authenticationService; + UserService = userService; GameService = gameService; InstallService = installService; ImportService = importService; @@ -301,9 +307,11 @@ namespace LANCommander.Launcher.Services private async Task ChangeAlias(ChangeAliasCommandLineOptions options) { + var currentUser = await UserService.GetAsync(AuthenticationService.GetUserId()); + await ProfileService.ChangeAlias(options.Alias); - Logger.LogInformation($"Changed current user's alias from {Settings.Profile.Alias} to {options.Alias}"); + Logger.LogInformation($"Changed current user's alias from {currentUser.Alias} to {options.Alias}"); } } } diff --git a/LANCommander.Launcher.Services/ImportService.cs b/LANCommander.Launcher.Services/ImportService.cs index f553eeba..fc9acbd4 100644 --- a/LANCommander.Launcher.Services/ImportService.cs +++ b/LANCommander.Launcher.Services/ImportService.cs @@ -31,16 +31,11 @@ namespace LANCommander.Launcher.Services { public class ImportService : BaseService { + private readonly AuthenticationService AuthenticationService; private readonly MediaService MediaService; - private readonly CollectionService CollectionService; - private readonly CompanyService CompanyService; private readonly EngineService EngineService; private readonly GameService GameService; - private readonly GenreService GenreService; - private readonly PlatformService PlatformService; - private readonly MultiplayerModeService MultiplayerModeService; - private readonly RedistributableService RedistributableService; - private readonly TagService TagService; + private readonly LibraryService LibraryService; private readonly MessageBusService MessageBusService; private readonly Settings Settings; private readonly DatabaseContext DatabaseContext; @@ -65,29 +60,19 @@ namespace LANCommander.Launcher.Services public ImportService( SDK.Client client, ILogger logger, + AuthenticationService authenticationService, + LibraryService libraryService, MediaService mediaService, - CollectionService collectionService, - CompanyService companyService, EngineService engineService, GameService gameService, - GenreService genreService, - PlatformService platformService, - MultiplayerModeService multiplayerModeService, - RedistributableService redistributableService, - TagService tagService, MessageBusService messageBusService, DatabaseContext databaseContext) : base(client, logger) { + AuthenticationService = authenticationService; + LibraryService = libraryService; MediaService = mediaService; - CollectionService = collectionService; - CompanyService = companyService; EngineService = engineService; GameService = gameService; - GenreService = genreService; - PlatformService = platformService; - MultiplayerModeService = multiplayerModeService; - RedistributableService = redistributableService; - TagService = tagService; MessageBusService = messageBusService; DatabaseContext = databaseContext; @@ -103,8 +88,7 @@ namespace LANCommander.Launcher.Services { try { - await ImportGamesAsync(); - await ImportRedistributables(); + await ImportLibraryAsync(); OnImportComplete?.Invoke(); } @@ -113,61 +97,15 @@ namespace LANCommander.Launcher.Services OnImportFailed?.Invoke(ex); } } - - private async Task> ImportBulkAsync( - ICollection target, - IEnumerable source, - Action updateAction) - where TModel : BaseModel - where TKeyedModel : IKeyedModel - { - foreach (var sourceItem in source) - { - var existingItem = await DatabaseContext.Set().FirstOrDefaultAsync(i => i.Id == sourceItem.Id); - - if (existingItem != null) - { - updateAction(existingItem, sourceItem); - - DatabaseContext.Update(existingItem); - - await DatabaseContext.SaveChangesAsync(); - } - else - { - // Add - var item = (TModel)Activator.CreateInstance(typeof(TModel)); - - item.Id = sourceItem.Id; - - updateAction(item, sourceItem); - - var result = await DatabaseContext.Set().AddAsync(item); - - await DatabaseContext.SaveChangesAsync(); - - target.Add(result.Entity); - } - } - - var toRemove = target.Where(x => !source.Any(y => y.Id == x.Id)).ToList(); - - foreach (var item in toRemove) - { - target.Remove(item); - } - - return target; - } - - private async Task ImportGameAsync(Guid id) + + public async Task ImportGameAsync(Guid id) { var game = await Client.Games.GetAsync(id); return await ImportGameAsync(game); } - private async Task ImportGameAsync(SDK.Models.Game game) + public async Task ImportGameAsync(SDK.Models.Game game) { using (var op = Logger.BeginOperation("Importing game {GameTitle}", game.Title)) { @@ -324,8 +262,6 @@ namespace LANCommander.Launcher.Services }) .AsNoRemove() .ImportAsync(); - - var mediaStoragePath = MediaService.GetStoragePath(); var importedMedia = await DatabaseContext.BulkImport() .SetTarget(localGame.Media) @@ -401,15 +337,17 @@ namespace LANCommander.Launcher.Services } } - public async Task ImportGamesAsync() + public async Task ImportLibraryAsync() { - var library = await Client.Library.GetAsync(); + var remoteLibrary = await Client.Library.GetAsync(); - await ImportGamesAsync(library); + await ImportLibraryAsync(remoteLibrary); } - public async Task ImportGamesAsync(IEnumerable games) + public async Task ImportLibraryAsync(IEnumerable games) { + var library = await LibraryService.GetByUserAsync(AuthenticationService.GetUserId()); + Logger?.LogInformation("Importing games"); int i = 1; @@ -428,7 +366,11 @@ namespace LANCommander.Launcher.Services Total = games.Count() }); - await ImportGameAsync(remoteGame); + var importedGame = await ImportGameAsync(remoteGame); + + library.Games.Add(importedGame); + + await LibraryService.UpdateAsync(library); } catch (Exception ex) { @@ -461,7 +403,7 @@ namespace LANCommander.Launcher.Services toImport.Add(libraryGame); } - await ImportGamesAsync(toImport); + await ImportLibraryAsync(toImport); } public async Task ImportRedistributables() diff --git a/LANCommander.Launcher.Services/LibraryService.cs b/LANCommander.Launcher.Services/LibraryService.cs index 9948140f..365e3e85 100644 --- a/LANCommander.Launcher.Services/LibraryService.cs +++ b/LANCommander.Launcher.Services/LibraryService.cs @@ -23,9 +23,10 @@ namespace LANCommander.Launcher.Services { public class LibraryService : BaseDatabaseService { + private readonly AuthenticationService AuthenticationService; private readonly InstallService InstallService; private readonly GameService GameService; - private readonly ImportService ImportService; + private readonly UserService UserService; public Dictionary RunningProcesses = new Dictionary(); @@ -46,16 +47,17 @@ namespace LANCommander.Launcher.Services DatabaseContext databaseContext, SDK.Client client, ILogger logger, + AuthenticationService authenticationService, InstallService installService, GameService gameService, - ImportService importService) : base(databaseContext, client, logger) + UserService userService) : base(databaseContext, client, logger) { + AuthenticationService = authenticationService; InstallService = installService; GameService = gameService; - ImportService = importService; + UserService = userService; InstallService.OnInstallComplete += InstallService_OnInstallComplete; - ImportService.OnImportComplete += ImportService_OnImportComplete; Filter.OnChanged += Filter_OnChanged; } @@ -65,13 +67,6 @@ namespace LANCommander.Launcher.Services await OnItemsFiltered.Invoke(Filter.ApplyFilter(Items)); } - private async Task ImportService_OnImportComplete() - { - await RefreshItemsAsync(); - - LibraryChanged(); - } - private async Task InstallService_OnInstallComplete(Game game) { if (OnLibraryChanged != null) @@ -95,11 +90,42 @@ namespace LANCommander.Launcher.Services public async Task GetByUserAsync(Guid userId) { - return Context - .Libraries - .Include(l => l.User) - .Include(l => l.Games) - .FirstOrDefault(l => l.User.Id == userId); + var user = await Context + .Users + .Include(u => u.Library) + .ThenInclude(l => l.Games) + .FirstOrDefaultAsync(u => u.Id == userId); + + try + { + if (user == null) + { + user = new User + { + Id = userId, + }; + + user = Context.Users.Add(user).Entity; + + await Context.SaveChangesAsync(); + } + + if (user.Library == null) + { + user.Library = new Library(); + + user = Context.Users.Update(user).Entity; + + await Context.SaveChangesAsync(); + } + } + catch (Exception ex) + { + + } + + + return user.Library; } public async Task> GetItemsAsync() @@ -108,22 +134,26 @@ namespace LANCommander.Launcher.Services using (var op = Logger.BeginOperation(LogLevel.Trace, "Loading library items from local database")) { - var games = await Context.Games - .Include(g => g.Collections) - .Include(g => g.Developers) - .Include(g => g.Genres) - .Include(g => g.Publishers) - .Include(g => g.Tags) - .Include(g => g.PlaySessions) - .Include(g => g.Engine) - .Include(g => g.Platforms) - .Include(g => g.Media) - .Include(g => g.MultiplayerModes) - .ToListAsync(); + var library = await GetByUserAsync(AuthenticationService.GetUserId()); + library = await Context + .Libraries + .AsQueryable() + .Include(l => l.Games).ThenInclude(g => g.Collections) + .Include(l => l.Games).ThenInclude(g => g.Collections) + .Include(l => l.Games).ThenInclude(g => g.Developers) + .Include(l => l.Games).ThenInclude(g => g.Genres) + .Include(l => l.Games).ThenInclude(g => g.Publishers) + .Include(l => l.Games).ThenInclude(g => g.Tags) + .Include(l => l.Games).ThenInclude(g => g.PlaySessions) + .Include(l => l.Games).ThenInclude(g => g.Engine) + .Include(l => l.Games).ThenInclude(g => g.Platforms) + .Include(l => l.Games).ThenInclude(g => g.Media) + .Include(l => l.Games).ThenInclude(g => g.MultiplayerModes) + .FirstOrDefaultAsync(l => l.UserId == AuthenticationService.GetUserId()); - Filter.Populate(games); + Filter.Populate(library.Games); - foreach (var item in games.Select(g => new ListItem(g)).OrderByTitle(g => !String.IsNullOrWhiteSpace(g.SortName) ? g.SortName : g.Name)) + foreach (var item in library.Games.Select(g => new ListItem(g)).OrderByTitle(g => !String.IsNullOrWhiteSpace(g.SortName) ? g.SortName : g.Name)) { Items.Add(item); } @@ -174,20 +204,31 @@ namespace LANCommander.Launcher.Services public async Task AddToLibraryAsync(Guid id) { - await Client.Library.AddToLibrary(id); + var localGame = await GameService.GetAsync(id); + var library = await GetByUserAsync(AuthenticationService.GetUserId()); - await ImportService.ImportGamesAsync(id); + if (localGame != null) + { + library.Games.Add(localGame); + } + + await UpdateAsync(library); + + await Client.Library.AddToLibrary(id); await LibraryChanged(); } public async Task RemoveFromLibraryAsync(Guid id) { - await Client.Library.RemoveFromLibrary(id); - var localGame = await GameService.GetAsync(id); + var library = await GetByUserAsync(AuthenticationService.GetUserId()); - await GameService.DeleteAsync(localGame); + library.Games.Remove(localGame); + + await UpdateAsync(library); + + await Client.Library.RemoveFromLibrary(id); await LibraryChanged(); } diff --git a/LANCommander.Launcher.Services/ProfileService.cs b/LANCommander.Launcher.Services/ProfileService.cs index e2e9cd5e..a48ca749 100644 --- a/LANCommander.Launcher.Services/ProfileService.cs +++ b/LANCommander.Launcher.Services/ProfileService.cs @@ -13,36 +13,49 @@ namespace LANCommander.Launcher.Services { public class ProfileService : BaseService { + private readonly AuthenticationService AuthenticationService; private readonly MediaService MediaService; + private readonly UserService UserService; private Settings Settings; public ProfileService( SDK.Client client, ILogger logger, - MediaService mediaService) : base(client, logger) + AuthenticationService authenticationService, + MediaService mediaService, + UserService userService) : base(client, logger) { + AuthenticationService = authenticationService; MediaService = mediaService; + UserService = userService; Settings = SettingService.GetSettings(); + + AuthenticationService.OnLogin += async (sender, args) => await DownloadProfileInfoAsync(); + AuthenticationService.OnRegister += async (sender, args) => await DownloadProfileInfoAsync(); } public async Task ChangeAlias(string newName) { + var currentUserId = AuthenticationService.GetUserId(); + var currentUser = await UserService.GetAsync(currentUserId); + await Client.Profile.ChangeAliasAsync(newName); - Settings = SettingService.GetSettings(); - - Settings.Profile.Alias = newName; - - SettingService.SaveSettings(Settings); + currentUser.Alias = newName; + + await UserService.UpdateAsync(currentUser); } public async Task DownloadProfileInfoAsync() { var remoteProfile = await Client.Profile.GetAsync(); - - Settings.Profile.Id = remoteProfile.Id; - Settings.Profile.Alias = String.IsNullOrWhiteSpace(remoteProfile.Alias) ? remoteProfile.UserName : remoteProfile.Alias; + var localUser = await UserService.AddMissingAsync(u => u.Id == remoteProfile.Id, new User + { + Id = remoteProfile.Id, + UserName = remoteProfile.UserName, + Alias = remoteProfile.Alias, + }); try { @@ -65,7 +78,9 @@ namespace LANCommander.Launcher.Services if (File.Exists(tempAvatarPath)) File.Move(tempAvatarPath, localPath); - Settings.Profile.AvatarId = media.Id; + localUser.Value.Avatar = media; + + await UserService.UpdateAsync(localUser.Value); } } catch (Exception ex) diff --git a/LANCommander.Launcher.Services/UserService.cs b/LANCommander.Launcher.Services/UserService.cs index 86c4810d..8c882d32 100644 --- a/LANCommander.Launcher.Services/UserService.cs +++ b/LANCommander.Launcher.Services/UserService.cs @@ -1,13 +1,36 @@ using LANCommander.Launcher.Data; using LANCommander.Launcher.Data.Models; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace LANCommander.Launcher.Services { public class UserService : BaseDatabaseService { - public UserService(DatabaseContext dbContext, SDK.Client client, ILogger logger) : base(dbContext, client, logger) + private readonly AuthenticationService AuthenticationService; + public UserService( + DatabaseContext dbContext, + SDK.Client client, + ILogger logger, + AuthenticationService authenticationService) : base(dbContext, client, logger) { + AuthenticationService = authenticationService; + } + + public async Task GetCurrentUser() + { + return await Context + .Users + .AsQueryable() + .Include(u => u.Avatar) + .FirstOrDefaultAsync(u => u.Id == AuthenticationService.GetUserId()); + } + + public async Task GetAliasAsync(Guid id) + { + var user = await GetAsync(id); + + return String.IsNullOrWhiteSpace(user.Alias) ? user.UserName : user.Alias; } } } diff --git a/LANCommander.Launcher/Styles/_layout.scss b/LANCommander.Launcher/Styles/_layout.scss index a12be45e..4a19f868 100644 --- a/LANCommander.Launcher/Styles/_layout.scss +++ b/LANCommander.Launcher/Styles/_layout.scss @@ -10,6 +10,18 @@ app { bottom: 0; } +// Hack for Depot spinner +.pb-custom-window-content > .ant-spin-nested-loading { + height: 100%; + overflow: hidden; + + & > div, + & > div > .ant-spin-container, + & .ant-layout { + height: 100%; + } +} + .ant-layout { flex-grow: 1; overflow: hidden; diff --git a/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationFormState.cs b/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationFormState.cs new file mode 100644 index 00000000..0b1583c5 --- /dev/null +++ b/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationFormState.cs @@ -0,0 +1,7 @@ +namespace LANCommander.Launcher.UI.Authenticate.Components; + +public class AuthenticationFormState +{ + public AuthenticationStage Stage { get; set; } + public string ServerAddress { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationStage.cs b/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationStage.cs index 42a83a75..5efaf6f7 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationStage.cs +++ b/LANCommander.Launcher/UI/Authenticate/Components/AuthenticationStage.cs @@ -1,8 +1,8 @@ namespace LANCommander.Launcher.UI.Authenticate.Components; -public static class AuthenticationStage +public enum AuthenticationStage { - public const string Login = "Login"; - public const string Register = "Register"; - public const string SelectServer = "SelectServer"; + Login, + Register, + SelectServer, } \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor b/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor index 57ee4911..2ec84136 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor +++ b/LANCommander.Launcher/UI/Authenticate/Components/LoginForm.razor @@ -5,14 +5,14 @@ @inject IMessageService MessageService @inject ILogger Logger - + Sign In
- + @@ -25,13 +25,6 @@ Login - @if (Settings.Profile.Id != Guid.Empty && !String.IsNullOrWhiteSpace(Settings.Profile.Alias)) - { - - } - @@ -68,7 +61,7 @@ @code { [Parameter] public string ServerAddress { get; set; } - [Parameter] public EventCallback ServerAddressChanged { get; set; } + [Parameter] public EventCallback OnBack { get; set; } List AuthenticationProviders = new(); diff --git a/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor b/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor index 23356c9e..e3c468fa 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor +++ b/LANCommander.Launcher/UI/Authenticate/Components/RegistrationForm.razor @@ -5,14 +5,14 @@ @inject IMessageService MessageService @inject ILogger Logger - + Register - + @@ -32,7 +32,7 @@ @code { [Parameter] public string ServerAddress { get; set; } - [Parameter] public EventCallback ServerAddressChanged { get; set; } + [Parameter] public EventCallback OnBack { get; set; } RegistrationRequest Model = new(); bool Loading = false; @@ -66,11 +66,6 @@ } } - void Back() - { - NavigationManager.NavigateTo("/Authenticate/Login"); - } - async Task UseToken(AuthToken token) { Settings.Authentication.AccessToken = token.AccessToken; diff --git a/LANCommander.Launcher/UI/Authenticate/Components/ServerSelector.razor b/LANCommander.Launcher/UI/Authenticate/Components/ServerSelector.razor index 1f6bbb24..084d4cea 100644 --- a/LANCommander.Launcher/UI/Authenticate/Components/ServerSelector.razor +++ b/LANCommander.Launcher/UI/Authenticate/Components/ServerSelector.razor @@ -17,11 +17,6 @@ - - @if (OfflineModeAvailable) - { - - } @@ -50,11 +45,12 @@ @code { - [Parameter] public string ServerAddress { get; set; } - [Parameter] public EventCallback ServerAddressChanged { get; set; } + [Parameter] public EventCallback OnSelected { get; set; } bool BeaconActive = false; bool OfflineModeAvailable = false; + + string ServerAddress = String.Empty; AuthRequest Model = new(); List DiscoveredServers = new(); @@ -70,25 +66,15 @@ async Task SelectServer(string serverAddress) { - ServerAddress = serverAddress; - Probe.Stop(); Probe.Dispose(); BeaconActive = false; - if (ServerAddressChanged.HasDelegate) - await ServerAddressChanged.InvokeAsync(ServerAddress); + if (OnSelected.HasDelegate) + await OnSelected.InvokeAsync(serverAddress); - NavigationManager.NavigateTo("Authenticate/Login"); - } - - async Task OfflineMode() - { - Settings.Authentication.OfflineMode = true; - SettingService.SaveSettings(Settings); - - NavigationManager.NavigateTo("/"); + await InvokeAsync(StateHasChanged); } async Task ActivateBeacon() diff --git a/LANCommander.Launcher/UI/Authenticate/Index.razor b/LANCommander.Launcher/UI/Authenticate/Index.razor deleted file mode 100644 index a51a6fe6..00000000 --- a/LANCommander.Launcher/UI/Authenticate/Index.razor +++ /dev/null @@ -1,44 +0,0 @@ -@page "/Authenticate" -@page "/Authenticate/{stage}" -@using System.Security.Policy -@using BeaconLib -@using LANCommander.Launcher.Models -@using LANCommander.SDK -@using LANCommander.SDK.Models -@using LANCommander.Launcher.UI.Authenticate.Components -@inject ProfileService ProfileService -@inject NavigationManager NavigationManager -@inject SDK.Client Client -@inject IMessageService MessageService -@inject ILogger Logger - - - - - -
- @if (Stage == AuthenticationStage.SelectServer || String.IsNullOrWhiteSpace(Stage)) - { - - } - - @if (Stage == AuthenticationStage.Login) - { - - } - - @if (Stage == AuthenticationStage.Register) - { - - } -
-
-
- -@code { - [Parameter] public string Stage { get; set; } - - string ServerAddress { get; set; } -} diff --git a/LANCommander.Launcher/UI/Components/AuthenticatedView.razor b/LANCommander.Launcher/UI/Components/AuthenticatedView.razor new file mode 100644 index 00000000..e7fb2cd8 --- /dev/null +++ b/LANCommander.Launcher/UI/Components/AuthenticatedView.razor @@ -0,0 +1,27 @@ +@using ConnectionState = LANCommander.Launcher.Models.ConnectionState +@inject AuthenticationService AuthenticationService +@inject NavigationManager NavigationManager + + + @if (ConnectionState.IsConnected || ConnectionState.OfflineModeEnabled) + { + @Authenticated + } + else + { + @NotAuthenticated + } + + +@code { + [Parameter] public RenderFragment Authenticated { get; set; } + [Parameter] public RenderFragment NotAuthenticated { get; set; } + + public ConnectionState ConnectionState = new(); + + protected override async Task OnInitializedAsync() + { + ConnectionState.IsConnected = await AuthenticationService.ValidateConnectionAsync(); + ConnectionState.OfflineModeEnabled = AuthenticationService.OfflineModeEnabled(); + } +} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Components/AuthenticationForm.razor b/LANCommander.Launcher/UI/Components/AuthenticationForm.razor new file mode 100644 index 00000000..2e4ac667 --- /dev/null +++ b/LANCommander.Launcher/UI/Components/AuthenticationForm.razor @@ -0,0 +1,57 @@ +@using System.Security.Policy +@using BeaconLib +@using LANCommander.Launcher.Models +@using LANCommander.SDK +@using LANCommander.SDK.Models +@using LANCommander.Launcher.UI.Authenticate.Components +@inject ProfileService ProfileService +@inject NavigationManager NavigationManager +@inject SDK.Client Client +@inject IMessageService MessageService +@inject ILogger Logger + + + + + + +
+ @if (State.Stage == AuthenticationStage.SelectServer) + { + + } + + @if (State.Stage == AuthenticationStage.Login) + { + + } + + @if (State.Stage == AuthenticationStage.Register) + { + + } +
+
+
+
+ +@code { + AuthenticationFormState State { get; set; } + + protected override void OnInitialized() + { + State = new() + { + Stage = AuthenticationStage.SelectServer, + ServerAddress = String.Empty, + }; + } + + void ServerSelected(string serverAddress) + { + State.ServerAddress = serverAddress; + State.Stage = AuthenticationStage.Login; + } +} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor b/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor index 9d1543a2..d8eff1d2 100644 --- a/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor +++ b/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor @@ -3,6 +3,7 @@ @using System.Diagnostics @using LANCommander.SDK.Helpers @inject GameService GameService +@inject UserService UserService @inject LibraryService LibraryService @inject InstallService InstallService @inject ModalService ModalService @@ -262,11 +263,12 @@ else async Task RunNameChangeScripts() { + var user = await UserService.GetCurrentUser(); var manifests = await Client.Games.GetManifestsAsync(Game.InstallDirectory, Game.Id); foreach (var manifest in manifests) { - await Client.Scripts.RunNameChangeScriptAsync(Game.InstallDirectory, Game.Id, Settings.Profile.Alias); + await Client.Scripts.RunNameChangeScriptAsync(Game.InstallDirectory, Game.Id, user.Alias); } } diff --git a/LANCommander.Launcher/UI/Components/ProfileButton.razor b/LANCommander.Launcher/UI/Components/ProfileButton.razor index 1af6b377..7a357a4f 100644 --- a/LANCommander.Launcher/UI/Components/ProfileButton.razor +++ b/LANCommander.Launcher/UI/Components/ProfileButton.razor @@ -1,4 +1,6 @@ -@using LANCommander.Launcher.Models +@using LANCommander.Launcher.Data.Models +@using LANCommander.Launcher.Models +@inject UserService UserService @inject ProfileService ProfileService @inject AuthenticationService AuthenticationService @inject NavigationManager NavigationManager @@ -25,12 +27,12 @@ @@ -39,15 +41,15 @@ [CascadingParameter] public ConnectionState ConnectionState { get; set; } Models.Settings Settings = null; + User User; + protected override async Task OnInitializedAsync() { - Settings = SettingService.GetSettings(); + User = await UserService.GetCurrentUser(); } async Task ChangeAlias() { - var settings = SettingService.GetSettings(); - var modalOptions = new ModalOptions() { Title = "Change Name", @@ -57,7 +59,7 @@ Centered = true }; - var modalRef = ModalService.CreateModal(modalOptions, settings.Profile.Alias); + var modalRef = ModalService.CreateModal(modalOptions, User.Alias); modalRef.OnOk = async (newName) => { diff --git a/LANCommander.Launcher/UI/Depot/Components/DepotGameDetails.razor b/LANCommander.Launcher/UI/Depot/Components/DepotGameDetails.razor index fbbc04f9..5ca55281 100644 --- a/LANCommander.Launcher/UI/Depot/Components/DepotGameDetails.razor +++ b/LANCommander.Launcher/UI/Depot/Components/DepotGameDetails.razor @@ -4,6 +4,7 @@ @inject LibraryService LibraryService @inject GameService GameService @inject InstallService InstallService +@inject ImportService ImportService @inject SDK.Client Client @inject ILogger Logger @inject NavigationManager NavigationManager @@ -41,7 +42,7 @@ @if (IsInstalled) { - + } else { @@ -248,7 +249,9 @@ await Task.Yield(); } - try { + try + { + await ImportService.ImportGameAsync(SelectedGame.Id); await LibraryService.AddToLibraryAsync(SelectedGame.Id); await LibraryService.RefreshItemsAsync(); @@ -261,8 +264,7 @@ Logger?.LogError(ex, $"{SelectedGame.Title} ({SelectedGame.Id}) could not be added to your library!"); MessageService.Error($"{SelectedGame.Title} could not be added to your library!"); } - - + AddingToLibrary = false; StateHasChanged(); await Task.Yield(); diff --git a/LANCommander.Launcher/UI/Depot/Components/DepotGameItem.razor b/LANCommander.Launcher/UI/Depot/Components/DepotGameItem.razor index 55d3c081..8df1a70b 100644 --- a/LANCommander.Launcher/UI/Depot/Components/DepotGameItem.razor +++ b/LANCommander.Launcher/UI/Depot/Components/DepotGameItem.razor @@ -2,6 +2,7 @@ @inject SDK.Client Client @inject DepotService DepotService @inject LibraryService LibraryService +@inject ImportService ImportService @inject IMessageService MessageService @inject ILogger Logger @@ -39,7 +40,9 @@ StateHasChanged(); await Task.Yield(); + await ImportService.ImportGameAsync(Item.Key); await LibraryService.AddToLibraryAsync(Item.Key); + await LibraryService.RefreshItemsAsync(); Importing = false; StateHasChanged(); diff --git a/LANCommander.Launcher/UI/Depot/Index.razor b/LANCommander.Launcher/UI/Depot/Index.razor index 19d986dd..a1c9f17d 100644 --- a/LANCommander.Launcher/UI/Depot/Index.razor +++ b/LANCommander.Launcher/UI/Depot/Index.razor @@ -1,34 +1,28 @@ @page "/Depot" @page "/Depot/{id:guid}" -@using LANCommander.Launcher.Data.Models -@using LANCommander.Launcher.Models.Enums @using LANCommander.Launcher.Models -@using System.Diagnostics @using LANCommander.Launcher.UI.Depot.Components -@using LANCommander.SDK.Helpers -@inject SDK.Client Client +@implements IDisposable @inject NavigationManager NavigationManager @inject DepotService DepotService -@inject InstallService InstallService -@inject GameService GameService -@inject ModalService ModalService -@inject ConfirmService ConfirmService -@inject ImportService ImportService +@inject KeepAliveService KeepAliveService - - - + + + + - - - + + + + + + - - @code { @@ -42,13 +36,36 @@ Settings Settings = SettingService.GetSettings(); + bool Disabled = false; + protected override async Task OnInitializedAsync() { DepotService.OnItemsFiltered += LoadFilteredItems; + + KeepAliveService.ConnectionSevered += KeepAliveServiceOnConnectionSevered; + KeepAliveService.ConnectionEstablished += KeepAliveServiceOnConnectionEstablished; + KeepAliveService.ConnectionLostPermanently += KeepAliveServiceOnConnectionLostPermanently; await DepotService.RefreshItemsAsync(); } + private void KeepAliveServiceOnConnectionLostPermanently(object? sender, EventArgs e) + { + NavigationManager.NavigateTo("/"); + } + + private async void KeepAliveServiceOnConnectionEstablished(object? sender, EventArgs e) + { + Disabled = false; + await InvokeAsync(StateHasChanged); + } + + private async void KeepAliveServiceOnConnectionSevered(object? sender, EventArgs e) + { + Disabled = true; + await InvokeAsync(StateHasChanged); + } + protected override async Task OnParametersSetAsync() { SelectedItem = DepotService.Items.FirstOrDefault(i => i.Key == Id); @@ -65,4 +82,12 @@ await InvokeAsync(StateHasChanged); } + + public void Dispose() + { + KeepAliveService.ConnectionEstablished -= KeepAliveServiceOnConnectionEstablished; + KeepAliveService.ConnectionSevered -= KeepAliveServiceOnConnectionSevered; + KeepAliveService.ConnectionLostPermanently -= KeepAliveServiceOnConnectionLostPermanently; + } + } diff --git a/LANCommander.Launcher/UI/Library/Index.razor b/LANCommander.Launcher/UI/Library/Index.razor index 2205d05b..bb8ba3be 100644 --- a/LANCommander.Launcher/UI/Library/Index.razor +++ b/LANCommander.Launcher/UI/Library/Index.razor @@ -186,7 +186,7 @@ InstallService.OnInstallFail += OnInstallFail; InstallService.OnInstallComplete += OnInstallComplete; GameService.OnUninstallComplete += OnUninstallComplete; - + await LoadData(); } diff --git a/LANCommander.Launcher/UI/MainLayout.razor b/LANCommander.Launcher/UI/MainLayout.razor index 2e0498b2..f891d665 100644 --- a/LANCommander.Launcher/UI/MainLayout.razor +++ b/LANCommander.Launcher/UI/MainLayout.razor @@ -13,12 +13,11 @@ @inject LANCommander.SDK.Client LANCommander @inject IJSRuntime JS - - - + + @@ -29,63 +28,49 @@ - - + + + + + + + - - - - - @context.StackTrace - - - - - + + + + + + + + + + - @code { Models.Settings Settings = null; - public bool Importing = false; - public bool Connecting = false; + public bool Importing; + public bool Connecting; public ConnectionState ConnectionState = new(); @@ -140,16 +125,10 @@ ImportService.OnImportUpdated += OnImportUpdated; AuthenticationService.OnOfflineModeChanged += OnOfflineModeChanged; - + var randIndex = new Random().Next(0, CrashQuips.Length - 1); RandomQuip = CrashQuips[randIndex]; - - ConnectionState.IsConnected = await LANCommander.ValidateTokenAsync(); - ConnectionState.OfflineModeEnabled = Settings.Authentication.OfflineMode; - - if (!ConnectionState.IsConnected && !ConnectionState.OfflineModeEnabled) - NavigationManager.NavigateTo("/Authenticate"); } async Task OnImportComplete() diff --git a/LANCommander.UI/Components/ErrorHandler.razor b/LANCommander.UI/Components/ErrorHandler.razor new file mode 100644 index 00000000..719cabd0 --- /dev/null +++ b/LANCommander.UI/Components/ErrorHandler.razor @@ -0,0 +1,75 @@ +@using Microsoft.JSInterop +@inject IJSRuntime JS +@inject NavigationManager NavigationManager + + + + @ChildContent + + + + + + + + + + @context.Message + @context.StackTrace + + + + + + +@code { + [Parameter] public RenderFragment ChildContent { get; set; } + + string RandomQuip = ""; + + string[] CrashQuips = new[] + { + "You Died.", + "Snake? SNAAAAAAKE!", + "WASTED", + "Major fracture detected", + "The past is a gaping hole. You try to run from it, but the more you run, the deeper, the darker, the bigger it gets.", + "Your town center has been destroyed", + "Your forces are under attack!", + "You have lost the lead", + "Terrorists Win", + "War... War never changes.", + "You have died of dysentery", + "You have failed to restore the books. The Ages are lost.", + "Player was splattered by a demon", + "Sure, blame it on your ISP", + "Baba is no more", + "Guests are complaining they are lost", + "The darkness has overcome you", + "Subject: Gordon Freeman. Status: Terminated", + "Mission failed: You were spotted.", + "Critical damage! Eject, eject!", + "Your minions are unhappy. They are leaving.", + "The Empire has triumphed", + "Your quest has ended in failure", + "You have been eaten by a grue", + "You no mess with Lo Wang!", + "Sam was killed. Serious carnage ensues.", + "Damn, those alien bastards are gonna pay for shooting up my ride" + }; + + protected override async Task OnInitializedAsync() + { + var randIndex = new Random().Next(0, CrashQuips.Length - 1); + + RandomQuip = CrashQuips[randIndex]; + } + + async Task CopyError(Exception ex) + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", ex.Message + "\n" + ex.StackTrace); + } +} \ No newline at end of file