Refactor launcher authentication, working import from depot

This commit is contained in:
Pat Hartl 2025-01-20 12:10:30 -06:00
parent dc4a49d71a
commit cbe363ac4f
30 changed files with 505 additions and 356 deletions

View file

@ -149,5 +149,6 @@ namespace LANCommander.Launcher.Data
public DbSet<Media>? Media { get; set; }
public DbSet<Library>? Libraries { get; set; }
public DbSet<User>? Users { get; set; }
}
}

View file

@ -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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -532,10 +532,9 @@ namespace LANCommander.Launcher.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("Alias")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("AvatarId")
b.Property<Guid?>("AvatarId")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
@ -545,14 +544,13 @@ namespace LANCommander.Launcher.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("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
}

View file

@ -6,31 +6,30 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace LANCommander.Launcher.Data.Migrations
{
/// <inheritdoc />
public partial class AddLibraries : Migration
public partial class AddUserLibraries : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "User",
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", nullable: false),
Alias = table.Column<string>(type: "TEXT", nullable: false),
AvatarId = table.Column<Guid>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", nullable: true),
Alias = table.Column<string>(type: "TEXT", nullable: true),
AvatarId = table.Column<Guid>(type: "TEXT", nullable: true),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedOn = table.Column<DateTime>(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");
}
}
}

View file

@ -529,10 +529,9 @@ namespace LANCommander.Launcher.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("Alias")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("AvatarId")
b.Property<Guid?>("AvatarId")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
@ -542,14 +541,13 @@ namespace LANCommander.Launcher.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("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
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }

View file

@ -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<AuthenticationService> logger,
ProfileService profileService) : base(client, logger)
ILogger<AuthenticationService> 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<bool> 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)

View file

@ -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<CommandLineService> 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}");
}
}
}

View file

@ -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<ImportService> 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<ICollection<TModel>> ImportBulkAsync<TModel, TKeyedModel>(
ICollection<TModel> target,
IEnumerable<TKeyedModel> source,
Action<TModel, TKeyedModel> updateAction)
where TModel : BaseModel
where TKeyedModel : IKeyedModel
{
foreach (var sourceItem in source)
{
var existingItem = await DatabaseContext.Set<TModel>().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<TModel>().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<Game> ImportGameAsync(Guid id)
public async Task<Game> ImportGameAsync(Guid id)
{
var game = await Client.Games.GetAsync(id);
return await ImportGameAsync(game);
}
private async Task<Game> ImportGameAsync(SDK.Models.Game game)
public async Task<Game> 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<Media, SDK.Models.Media>()
.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<SDK.Models.EntityReference> games)
public async Task ImportLibraryAsync(IEnumerable<SDK.Models.EntityReference> 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()

View file

@ -23,9 +23,10 @@ namespace LANCommander.Launcher.Services
{
public class LibraryService : BaseDatabaseService<Library>
{
private readonly AuthenticationService AuthenticationService;
private readonly InstallService InstallService;
private readonly GameService GameService;
private readonly ImportService ImportService;
private readonly UserService UserService;
public Dictionary<Guid, Process> RunningProcesses = new Dictionary<Guid, Process>();
@ -46,16 +47,17 @@ namespace LANCommander.Launcher.Services
DatabaseContext databaseContext,
SDK.Client client,
ILogger<LibraryService> 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<Library> 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<IEnumerable<ListItem>> 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();
}

View file

@ -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<ProfileService> 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)

View file

@ -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<User>
{
public UserService(DatabaseContext dbContext, SDK.Client client, ILogger<UserService> logger) : base(dbContext, client, logger)
private readonly AuthenticationService AuthenticationService;
public UserService(
DatabaseContext dbContext,
SDK.Client client,
ILogger<UserService> logger,
AuthenticationService authenticationService) : base(dbContext, client, logger)
{
AuthenticationService = authenticationService;
}
public async Task<User> GetCurrentUser()
{
return await Context
.Users
.AsQueryable()
.Include(u => u.Avatar)
.FirstOrDefaultAsync(u => u.Id == AuthenticationService.GetUserId());
}
public async Task<string> GetAliasAsync(Guid id)
{
var user = await GetAsync(id);
return String.IsNullOrWhiteSpace(user.Alias) ? user.UserName : user.Alias;
}
}
}

View file

@ -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;

View file

@ -0,0 +1,7 @@
namespace LANCommander.Launcher.UI.Authenticate.Components;
public class AuthenticationFormState
{
public AuthenticationStage Stage { get; set; }
public string ServerAddress { get; set; }
}

View file

@ -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,
}

View file

@ -5,14 +5,14 @@
@inject IMessageService MessageService
@inject ILogger<LoginForm> Logger
<PageHeader OnBack="Back">
<PageHeader OnBack="OnBack">
<TitleTemplate>
Sign In
</TitleTemplate>
</PageHeader>
<Form Model="@Model" Loading="@Loading" Layout="FormLayout.Vertical" OnFinish="OnFinish">
<FormItem Label="Server Address">
<Input @bind-Value="@ServerAddress" Disabled />
<Input Value="ServerAddress" Disabled />
</FormItem>
<FormItem Label="Username">
<Input @bind-Value="@context.UserName"/>
@ -25,13 +25,6 @@
Login
</Button>
@if (Settings.Profile.Id != Guid.Empty && !String.IsNullOrWhiteSpace(Settings.Profile.Alias))
{
<Button OnClick="OfflineMode">
Offline
</Button>
}
<Button Type="ButtonType.Text" OnClick="@(() => NavigationManager.NavigateTo("/Authenticate/Register"))">
Register
</Button>
@ -68,7 +61,7 @@
@code {
[Parameter] public string ServerAddress { get; set; }
[Parameter] public EventCallback<string> ServerAddressChanged { get; set; }
[Parameter] public EventCallback OnBack { get; set; }
List<AuthenticationProvider> AuthenticationProviders = new();

View file

@ -5,14 +5,14 @@
@inject IMessageService MessageService
@inject ILogger<RegistrationForm> Logger
<PageHeader OnBack="Back">
<PageHeader OnBack="OnBack">
<TitleTemplate>
Register
</TitleTemplate>
</PageHeader>
<Form Model="@Model" Loading="@Loading" Layout="FormLayout.Vertical" OnFinish="OnFinish">
<FormItem Label="Server Address">
<Input @bind-Value="@ServerAddress" Disabled />
<Input Value="ServerAddress" Disabled />
</FormItem>
<FormItem Label="Username">
<Input @bind-Value="@context.UserName"/>
@ -32,7 +32,7 @@
@code {
[Parameter] public string ServerAddress { get; set; }
[Parameter] public EventCallback<string> 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;

View file

@ -17,11 +17,6 @@
<Flex Gap="FlexGap.Small">
<Input @bind-Value="ServerAddress" Placeholder="Server Address"/>
<Button Type="ButtonType.Primary" OnClick="() => SelectServer(ServerAddress)">Connect</Button>
@if (OfflineModeAvailable)
{
<Button OnClick="() => OfflineMode()">Offline</Button>
}
</Flex>
</FormItem>
</Form>
@ -50,11 +45,12 @@
</AntList>
@code {
[Parameter] public string ServerAddress { get; set; }
[Parameter] public EventCallback<string> ServerAddressChanged { get; set; }
[Parameter] public EventCallback<string> OnSelected { get; set; }
bool BeaconActive = false;
bool OfflineModeAvailable = false;
string ServerAddress = String.Empty;
AuthRequest Model = new();
List<DiscoveredServer> 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()

View file

@ -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<System.Index> Logger
<Layout Style="background-image: url('/assets/auth-background.jpg'); background-size: cover;">
<Content Class="authentication-form">
<div class="authentication-logo">
<img src="/assets/logo.svg" width="300" />
</div>
<div class="authentication-box">
@if (Stage == AuthenticationStage.SelectServer || String.IsNullOrWhiteSpace(Stage))
{
<ServerSelector @bind-ServerAddress="ServerAddress" />
}
@if (Stage == AuthenticationStage.Login)
{
<LoginForm @bind-ServerAddress="ServerAddress" />
}
@if (Stage == AuthenticationStage.Register)
{
<RegistrationForm @bind-ServerAddress="ServerAddress" />
}
</div>
</Content>
</Layout>
@code {
[Parameter] public string Stage { get; set; }
string ServerAddress { get; set; }
}

View file

@ -0,0 +1,27 @@
@using ConnectionState = LANCommander.Launcher.Models.ConnectionState
@inject AuthenticationService AuthenticationService
@inject NavigationManager NavigationManager
<CascadingValue Value="ConnectionState">
@if (ConnectionState.IsConnected || ConnectionState.OfflineModeEnabled)
{
@Authenticated
}
else
{
@NotAuthenticated
}
</CascadingValue>
@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();
}
}

View file

@ -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<System.Index> Logger
<CascadingValue Value="State">
<Layout Style="background-image: url('/assets/auth-background.jpg'); background-size: cover;">
<Content Class="authentication-form">
<div class="authentication-logo">
<img src="/assets/logo.svg" width="300" />
</div>
<div class="authentication-box">
@if (State.Stage == AuthenticationStage.SelectServer)
{
<ServerSelector OnSelected="ServerSelected" />
}
@if (State.Stage == AuthenticationStage.Login)
{
<LoginForm ServerAddress="@State.ServerAddress" OnBack="() => State.Stage = AuthenticationStage.SelectServer" />
}
@if (State.Stage == AuthenticationStage.Register)
{
<RegistrationForm ServerAddress="@State.ServerAddress" OnBack="() => State.Stage = AuthenticationStage.Login" />
}
</div>
</Content>
</Layout>
</CascadingValue>
@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;
}
}

View file

@ -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);
}
}

View file

@ -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 @@
<ChildContent>
<Button Type="ButtonType.Primary" Class="appbar-profile-button">
@if (Settings.Profile.AvatarId != null && Settings.Profile.AvatarId != Guid.Empty)
@if (User != null && User.Avatar != null)
{
<MediaImage Id="@Settings.Profile.AvatarId" />
<MediaImage Id="@User.Avatar.Id" />
}
<span>@Settings.Profile.Alias</span>
<span>@User?.Alias</span>
</Button>
</ChildContent>
</Dropdown>
@ -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<ChangeAliasDialog, string, string>(modalOptions, settings.Profile.Alias);
var modalRef = ModalService.CreateModal<ChangeAliasDialog, string, string>(modalOptions, User.Alias);
modalRef.OnOk = async (newName) =>
{

View file

@ -4,6 +4,7 @@
@inject LibraryService LibraryService
@inject GameService GameService
@inject InstallService InstallService
@inject ImportService ImportService
@inject SDK.Client Client
@inject ILogger<DepotGameDetails> Logger
@inject NavigationManager NavigationManager
@ -41,7 +42,7 @@
<SpaceItem>
@if (IsInstalled)
{
<Button Type="ButtonType.Primary" Size="ButtonSize.Large" OnClick="@(() => NavigationManager.NavigateTo($"/{SelectedGame.Id}"))">View in Library</Button>
<Button Type="ButtonType.Primary" Size="ButtonSize.Large" OnClick="@(() => NavigationManager.NavigateTo($"{SelectedGame.Id}"))">View in Library</Button>
}
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();

View file

@ -2,6 +2,7 @@
@inject SDK.Client Client
@inject DepotService DepotService
@inject LibraryService LibraryService
@inject ImportService ImportService
@inject IMessageService MessageService
@inject ILogger<DepotGame> 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();

View file

@ -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
<Layout>
<Content Class="depot">
<DepotFilter Items="@ListItems" />
<Spin Spinning="Disabled">
<Layout>
<Content Class="depot">
<DepotFilter Items="@ListItems" />
<DepotList Items="@ListItems" OnItemSelected="@((item) => NavigationManager.NavigateTo($"/Depot/{item.Key}"))" />
</Content>
</Layout>
<DepotList Items="@ListItems" OnItemSelected="@((item) => NavigationManager.NavigateTo($"/Depot/{item.Key}"))" />
</Content>
</Layout>
<DepotGameDetails @ref="DepotGameDetails" ItemId="@(SelectedItem?.Key ?? Guid.Empty)" OnClose="@(() => NavigationManager.NavigateTo("/Depot"))" />
</Spin>
<div class="logo">
<img src="assets/logo-cut.svg" />
</div>
<DepotGameDetails @ref="DepotGameDetails" ItemId="@(SelectedItem?.Key ?? Guid.Empty)" OnClose="@(() => NavigationManager.NavigateTo("/Depot"))" />
<LANCommander.Launcher.UI.Components.Footer />
@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;
}
}

View file

@ -186,7 +186,7 @@
InstallService.OnInstallFail += OnInstallFail;
InstallService.OnInstallComplete += OnInstallComplete;
GameService.OnUninstallComplete += OnUninstallComplete;
await LoadData();
}

View file

@ -13,12 +13,11 @@
@inject LANCommander.SDK.Client LANCommander
@inject IJSRuntime JS
<CascadingValue Value="ConnectionState">
<CustomWindow HeaderHeight="37">
<HeaderExtraControlsLayout>
<Space Direction="SpaceDirection.Horizontal">
<ConnectionStateView>
<Online>
<AuthenticatedView>
<Authenticated>
<SpaceItem>
<Popover Placement="Placement.BottomRight" IsButton OnClick="Import" Trigger="new[] { Trigger.Hover }">
<ChildContent>
@ -29,63 +28,49 @@
</ContentTemplate>
</Popover>
</SpaceItem>
</Online>
<Offline>
<SpaceItem>
<ProfileButton/>
</SpaceItem>
</Authenticated>
<NotAuthenticated>
<SpaceItem>
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.CloudSync" OnClick="Connect" Loading="@Connecting" Danger/>
</SpaceItem>
</Offline>
</ConnectionStateView>
@if (Settings != null && Settings.Profile != null && ProfileService.IsAuthenticated())
{
<SpaceItem>
<ProfileButton />
</SpaceItem>
}
</NotAuthenticated>
</AuthenticatedView>
</Space>
</HeaderExtraControlsLayout>
<WindowContent>
<ErrorBoundary>
<ChildContent>
@Body
<ErrorHandler>
<AuthenticatedView>
<Authenticated>
@Body
@if (Settings.Debug.EnableScriptDebugging)
{
<PowerShellConsole />
}
@if (Settings.Debug.EnableScriptDebugging)
{
<PowerShellConsole/>
}
<UpdateChecker />
<AntContainer />
<KeepAliveContainer />
<RedirectToLogin />
</ChildContent>
<ErrorContent>
<Result Status="ResultStatus.Error"
Title="Launcher Crashed"
SubTitle="@RandomQuip"
Class="crash-error">
<Extra>
<Button Type="ButtonType.Primary" OnClick="@(() => NavigationManager.NavigateTo("/", true))">View Library</Button>
<Button OnClick="() => CopyError(context)">Copy Error</Button>
</Extra>
<ChildContent>
<code>
@context.StackTrace
</code>
</ChildContent>
</Result>
</ErrorContent>
</ErrorBoundary>
<KeepAliveContainer />
</Authenticated>
<NotAuthenticated>
<AuthenticationForm />
</NotAuthenticated>
</AuthenticatedView>
<UpdateChecker />
<AntContainer />
</ErrorHandler>
</WindowContent>
</CustomWindow>
</CascadingValue>
@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()

View file

@ -0,0 +1,75 @@
@using Microsoft.JSInterop
@inject IJSRuntime JS
@inject NavigationManager NavigationManager
<ErrorBoundary>
<ChildContent>
@ChildContent
</ChildContent>
<ErrorContent>
<Result Status="ResultStatus.Error"
Title="Launcher Crashed"
SubTitle="@RandomQuip"
Class="crash-error">
<Extra>
<Button Type="ButtonType.Primary" OnClick="@(() => NavigationManager.NavigateTo("/", true))">View Library</Button>
<Button OnClick="() => CopyError(context)">Copy Error</Button>
</Extra>
<ChildContent>
<code>
@context.Message
@context.StackTrace
</code>
</ChildContent>
</Result>
</ErrorContent>
</ErrorBoundary>
@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);
}
}