Packager app improvements, metadata endpoints
- Add ability to connect to server - Add uploading of finished package to server - Add metadata lookup when connected to server - Fix layout when specifying action - Hide finish button at generate step
This commit is contained in:
parent
90d04d1e5e
commit
819cb28d17
23 changed files with 1025 additions and 49 deletions
|
|
@ -122,7 +122,7 @@
|
|||
<ItemGroup Label="Microsoft Extensions">
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public partial class App : Application
|
|||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow(Program.Context);
|
||||
desktop.MainWindow = new MainWindow(Program.Context, Program.Services);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,14 @@
|
|||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="CommandLineParser" />
|
||||
<PackageReference Include="LANCommander.Interposer" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,24 @@
|
|||
Background="{DynamicResource BackgroundBrush}"
|
||||
Icon="avares://LANCommander.Packager/LANCommanderDark.ico">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- Server connection bar -->
|
||||
<Border Grid.Row="0" Background="#1A1A1A" BorderBrush="#2A2A2A"
|
||||
BorderThickness="0,0,0,1" Padding="12,6">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="8">
|
||||
<Ellipse Name="StatusDot" Width="8" Height="8" Fill="#555555" VerticalAlignment="Center" />
|
||||
<TextBlock Name="AuthStatusLabel" Text="Not connected"
|
||||
VerticalAlignment="Center" Opacity="0.5"
|
||||
Foreground="{DynamicResource TextPrimaryBrush}" FontSize="12" />
|
||||
</StackPanel>
|
||||
<Button Name="ConnectButton" Content="Connect to Server"
|
||||
HorizontalAlignment="Right" FontSize="12" Padding="12,4" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Main area -->
|
||||
<Grid ColumnDefinitions="220,*">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="220,*">
|
||||
<!-- Step sidebar -->
|
||||
<Border Background="#1A1A1A" BorderBrush="#2A2A2A" BorderThickness="0,0,1,0">
|
||||
<ItemsControl Name="StepList" Margin="24,0" VerticalAlignment="Center">
|
||||
|
|
@ -72,7 +87,7 @@
|
|||
</Grid>
|
||||
|
||||
<!-- Bottom navigation -->
|
||||
<Border Grid.Row="1" Background="#1A1A1A" BorderBrush="#2A2A2A"
|
||||
<Border Grid.Row="2" Background="#1A1A1A" BorderBrush="#2A2A2A"
|
||||
BorderThickness="0,1,0,0" Padding="20,12">
|
||||
<Grid>
|
||||
<Button Name="CancelButton" Content="Cancel" HorizontalAlignment="Left" />
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using LANCommander.Packager.Models;
|
||||
using LANCommander.Packager.Views;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Clients;
|
||||
using LANCommander.SDK.Factories;
|
||||
using LANCommander.SDK.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LANCommander.Packager;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly PackageContext _context;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly UserControl[] _steps;
|
||||
private readonly string[] _stepTitles;
|
||||
private readonly string[] _stepHelps;
|
||||
private readonly ObservableCollection<WizardStepItem> _stepItems;
|
||||
private int _currentStep;
|
||||
private bool _monitoringComplete;
|
||||
private bool _isAuthenticated;
|
||||
|
||||
private readonly MonitoringView _monitoringView;
|
||||
private readonly InstallDirectoryView _installDirView;
|
||||
|
|
@ -26,20 +34,35 @@ public partial class MainWindow : Window
|
|||
private readonly ActionView _actionView;
|
||||
private readonly OutputView _outputView;
|
||||
|
||||
public MainWindow() : this(new PackageContext()) { }
|
||||
private readonly AuthenticationClient _authClient;
|
||||
private readonly IConnectionClient _connectionClient;
|
||||
private readonly ISettingsProvider _settingsProvider;
|
||||
private readonly ITokenProvider _tokenProvider;
|
||||
private readonly MetadataClient _metadataClient;
|
||||
private readonly ApiRequestFactory _apiRequestFactory;
|
||||
|
||||
public MainWindow(PackageContext context)
|
||||
public MainWindow() : this(new PackageContext(), null!) { }
|
||||
|
||||
public MainWindow(PackageContext context, IServiceProvider services)
|
||||
{
|
||||
_context = context;
|
||||
_services = services;
|
||||
InitializeComponent();
|
||||
|
||||
_authClient = _services.GetRequiredService<AuthenticationClient>();
|
||||
_connectionClient = _services.GetRequiredService<IConnectionClient>();
|
||||
_settingsProvider = _services.GetRequiredService<ISettingsProvider>();
|
||||
_tokenProvider = _services.GetRequiredService<ITokenProvider>();
|
||||
_metadataClient = _services.GetRequiredService<MetadataClient>();
|
||||
_apiRequestFactory = _services.GetRequiredService<ApiRequestFactory>();
|
||||
|
||||
_monitoringView = new MonitoringView(context);
|
||||
_installDirView = new InstallDirectoryView(context);
|
||||
_fileSelectionView = new FileSelectionView(context);
|
||||
_registrySelectionView = new RegistrySelectionView(context);
|
||||
_metadataView = new MetadataView(context);
|
||||
_metadataView = new MetadataView(context, _metadataClient);
|
||||
_actionView = new ActionView(context);
|
||||
_outputView = new OutputView(context);
|
||||
_outputView = new OutputView(context, _apiRequestFactory, _settingsProvider);
|
||||
|
||||
_monitoringView.MonitoringCompleted += () =>
|
||||
{
|
||||
|
|
@ -80,6 +103,7 @@ public partial class MainWindow : Window
|
|||
BackButton.Click += OnBackClick;
|
||||
NextButton.Click += OnNextClick;
|
||||
CancelButton.Click += (_, _) => Close();
|
||||
ConnectButton.Click += OnConnectClick;
|
||||
|
||||
GoToStep(0);
|
||||
}
|
||||
|
|
@ -87,9 +111,73 @@ public partial class MainWindow : Window
|
|||
protected override async void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
await CheckExistingAuthAsync();
|
||||
await StartFirstStep();
|
||||
}
|
||||
|
||||
private async Task CheckExistingAuthAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = _tokenProvider.GetToken();
|
||||
|
||||
if (token == null || string.IsNullOrEmpty(token.AccessToken))
|
||||
return;
|
||||
|
||||
var serverAddress = _settingsProvider.CurrentValue.Authentication.ServerAddress;
|
||||
|
||||
if (serverAddress != null)
|
||||
await _connectionClient.UpdateServerAddressAsync(serverAddress);
|
||||
|
||||
var valid = await _authClient.ValidateTokenAsync();
|
||||
|
||||
if (valid)
|
||||
SetAuthenticated(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Token expired or server unreachable - stay disconnected
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAuthenticated(bool authenticated)
|
||||
{
|
||||
_isAuthenticated = authenticated;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (authenticated)
|
||||
{
|
||||
var address = _settingsProvider.CurrentValue.Authentication.ServerAddress;
|
||||
AuthStatusLabel.Text = $"Connected to {address}";
|
||||
AuthStatusLabel.Opacity = 0.8;
|
||||
StatusDot.Fill = new SolidColorBrush(Color.Parse("#49AA19"));
|
||||
ConnectButton.Content = "Server Settings";
|
||||
}
|
||||
else
|
||||
{
|
||||
AuthStatusLabel.Text = "Not connected";
|
||||
AuthStatusLabel.Opacity = 0.5;
|
||||
StatusDot.Fill = new SolidColorBrush(Color.Parse("#555555"));
|
||||
ConnectButton.Content = "Connect to Server";
|
||||
}
|
||||
|
||||
_metadataView.SetAuthenticated(authenticated);
|
||||
_outputView.SetAuthenticated(authenticated);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnConnectClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new ConnectDialog(_authClient, _connectionClient, _settingsProvider);
|
||||
var result = await dialog.ShowDialog<bool?>(this);
|
||||
|
||||
if (result == true && dialog.IsAuthenticated)
|
||||
SetAuthenticated(true);
|
||||
else if (!dialog.IsAuthenticated)
|
||||
SetAuthenticated(false);
|
||||
}
|
||||
|
||||
private async Task StartFirstStep()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_context.InstallerPath))
|
||||
|
|
@ -166,7 +254,7 @@ public partial class MainWindow : Window
|
|||
}
|
||||
|
||||
BackButton.IsVisible = step > 0;
|
||||
NextButton.Content = step == _steps.Length - 1 ? "Finish" : "Next";
|
||||
NextButton.IsVisible = step != _steps.Length - 1;
|
||||
NextButton.IsEnabled = step != 0 || _monitoringComplete;
|
||||
|
||||
EnterStep(step);
|
||||
|
|
|
|||
5
LANCommander.Packager/Models/PackagerSettings.cs
Normal file
5
LANCommander.Packager/Models/PackagerSettings.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace LANCommander.Packager.Models;
|
||||
|
||||
public class PackagerSettings : LANCommander.SDK.Models.Settings
|
||||
{
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
using Avalonia;
|
||||
using CommandLine;
|
||||
using LANCommander.Packager.Models;
|
||||
using LANCommander.SDK.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.Packager;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static PackageContext Context { get; } = new();
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
|
|
@ -16,14 +21,41 @@ internal static class Program
|
|||
{
|
||||
if (!string.IsNullOrWhiteSpace(options.InstallerPath))
|
||||
Context.InstallerPath = options.InstallerPath;
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.OutputPath))
|
||||
Context.OutputPath = options.OutputPath;
|
||||
});
|
||||
|
||||
ConfigureServices();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
private static void ConfigureServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Warning);
|
||||
});
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
var configurationBuilder = new ConfigurationBuilder();
|
||||
var configuration = configurationBuilder.ReadFromFile<PackagerSettings>();
|
||||
var refresher = configurationBuilder.ReadFromServer<PackagerSettings>(configuration);
|
||||
configuration = configurationBuilder.Build();
|
||||
|
||||
services.Configure<PackagerSettings>(configuration);
|
||||
services.AddSingleton(refresher);
|
||||
|
||||
services.AddLANCommanderClient<PackagerSettings>();
|
||||
|
||||
Services = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
|
|
|
|||
|
|
@ -258,27 +258,43 @@ public class InstallerMonitorService : IDisposable
|
|||
if (filePaths.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var directoryGroups = filePaths
|
||||
.GroupBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ToList();
|
||||
// Filter to non-system, non-ignored paths first
|
||||
var nonSystemPaths = filePaths.Where(p => !IsIgnoredPath(p) && !IsSystemPath(p)).ToList();
|
||||
|
||||
foreach (var group in directoryGroups)
|
||||
if (nonSystemPaths.Count > 0)
|
||||
{
|
||||
var dir = group.Key;
|
||||
|
||||
if (!IsIgnoredPath(dir) && !IsSystemPath(dir))
|
||||
return dir;
|
||||
// Find the common ancestor of all game file paths. This handles
|
||||
// installers that write many files into deep subdirectories
|
||||
// (e.g. Sounds/, _CD/SETUP/) — picking the deepest single
|
||||
// directory with the most files would miss the actual root.
|
||||
var ancestor = FindCommonAncestor(nonSystemPaths);
|
||||
|
||||
// If the ancestor is meaningful (not just a drive root like "G:\"),
|
||||
// use it. Otherwise fall back to the most-frequent directory.
|
||||
if (!string.IsNullOrEmpty(ancestor) && !IsDriveRoot(ancestor))
|
||||
return ancestor;
|
||||
|
||||
// Fall back: pick the most frequent non-system directory
|
||||
var directoryGroups = nonSystemPaths
|
||||
.GroupBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ToList();
|
||||
|
||||
if (directoryGroups.Count > 0)
|
||||
return directoryGroups[0].Key;
|
||||
}
|
||||
|
||||
var nonSystemPaths = filePaths.Where(p => !IsIgnoredPath(p) && !IsSystemPath(p)).ToList();
|
||||
|
||||
if (nonSystemPaths.Count > 0)
|
||||
return FindCommonAncestor(nonSystemPaths);
|
||||
|
||||
return filePaths.First();
|
||||
}
|
||||
|
||||
private static bool IsDriveRoot(string path)
|
||||
{
|
||||
var root = Path.GetPathRoot(path);
|
||||
return string.Equals(path.TrimEnd(Path.DirectorySeparatorChar),
|
||||
root?.TrimEnd(Path.DirectorySeparatorChar),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string FindCommonAncestor(List<string> paths)
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,29 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LANCommander.Packager.Views.ActionView">
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto"
|
||||
Margin="0,16,0,0">
|
||||
<TextBlock Text="Action Name" Opacity="0.7" Grid.Row="0" Grid.Column="0"
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<!-- Executable list -->
|
||||
<DockPanel Grid.Row="0">
|
||||
<TextBlock DockPanel.Dock="Top" Text="Select primary executable:" Opacity="0.7"
|
||||
Margin="0,0,0,4" />
|
||||
<TextBlock Text="Arguments" Opacity="0.7" Grid.Row="0" Grid.Column="1"
|
||||
Margin="8,0,0,4" />
|
||||
<TextBox Name="ActionNameField" Text="Play" Grid.Row="1" Grid.Column="0"
|
||||
Width="150" />
|
||||
<TextBox Name="ArgumentsField" Grid.Row="1" Grid.Column="1"
|
||||
Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Select primary executable:" Opacity="0.7" />
|
||||
<ListBox Name="ExeList"
|
||||
Background="Transparent"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#2A2A2A"
|
||||
CornerRadius="4" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Action definition -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto"
|
||||
Margin="0,16,0,0">
|
||||
<TextBlock Text="Action Name" Opacity="0.7" Grid.Row="0" Grid.Column="0"
|
||||
Margin="0,0,0,4" />
|
||||
<TextBlock Text="Arguments" Opacity="0.7" Grid.Row="0" Grid.Column="1"
|
||||
Margin="8,0,0,4" />
|
||||
<TextBox Name="ActionNameField" Grid.Row="1" Grid.Column="0"
|
||||
Width="200" />
|
||||
<TextBox Name="ArgumentsField" Grid.Row="1" Grid.Column="1"
|
||||
Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ public partial class ActionView : UserControl
|
|||
var bestGuess = FindBestExecutable();
|
||||
ExeList.SelectedIndex = bestGuess;
|
||||
}
|
||||
|
||||
var title = _context.Manifest.Title;
|
||||
ActionNameField.Text = string.IsNullOrWhiteSpace(title) ? "Play" : $"Play {title}";
|
||||
}
|
||||
|
||||
public void ApplyAction()
|
||||
|
|
|
|||
39
LANCommander.Packager/Views/ConnectDialog.axaml
Normal file
39
LANCommander.Packager/Views/ConnectDialog.axaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LANCommander.Packager.Views.ConnectDialog"
|
||||
Title="Connect to Server"
|
||||
Width="400" Height="320"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Background="{DynamicResource BackgroundBrush}">
|
||||
|
||||
<StackPanel Margin="24" Spacing="16">
|
||||
<TextBlock Text="Connect to LANCommander Server" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimaryBrush}" />
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Server Address" Opacity="0.7" />
|
||||
<TextBox Name="ServerAddressField" Watermark="http://192.168.1.100:1337" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Username" Opacity="0.7" />
|
||||
<TextBox Name="UsernameField" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Password" Opacity="0.7" />
|
||||
<TextBox Name="PasswordField" PasswordChar="*" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Name="ErrorLabel" Foreground="{DynamicResource ErrorBrush}"
|
||||
TextWrapping="Wrap" IsVisible="False" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,8,0,0">
|
||||
<Button Name="DisconnectButton" Content="Disconnect" Grid.Column="0"
|
||||
HorizontalAlignment="Left" IsVisible="False" />
|
||||
<Button Name="CancelButton" Content="Cancel" Grid.Column="1" Margin="0,0,8,0" />
|
||||
<Button Name="ConnectButton" Content="Connect" Grid.Column="2" Classes="Primary" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
109
LANCommander.Packager/Views/ConnectDialog.axaml.cs
Normal file
109
LANCommander.Packager/Views/ConnectDialog.axaml.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Clients;
|
||||
using LANCommander.SDK.Services;
|
||||
|
||||
namespace LANCommander.Packager.Views;
|
||||
|
||||
public partial class ConnectDialog : Window
|
||||
{
|
||||
private readonly AuthenticationClient _authClient;
|
||||
private readonly IConnectionClient _connectionClient;
|
||||
private readonly ISettingsProvider _settingsProvider;
|
||||
|
||||
public bool IsAuthenticated { get; private set; }
|
||||
|
||||
public ConnectDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ConnectDialog(
|
||||
AuthenticationClient authClient,
|
||||
IConnectionClient connectionClient,
|
||||
ISettingsProvider settingsProvider)
|
||||
{
|
||||
_authClient = authClient;
|
||||
_connectionClient = connectionClient;
|
||||
_settingsProvider = settingsProvider;
|
||||
InitializeComponent();
|
||||
|
||||
var currentAddress = _settingsProvider.CurrentValue.Authentication.ServerAddress;
|
||||
if (currentAddress != null)
|
||||
ServerAddressField.Text = currentAddress.ToString();
|
||||
|
||||
CancelButton.Click += (_, _) => Close(false);
|
||||
ConnectButton.Click += OnConnectClick;
|
||||
DisconnectButton.Click += OnDisconnectClick;
|
||||
|
||||
var token = _settingsProvider.CurrentValue.Authentication.Token;
|
||||
if (token != null && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
DisconnectButton.IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnConnectClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ErrorLabel.IsVisible = false;
|
||||
ConnectButton.IsEnabled = false;
|
||||
ConnectButton.Content = "Connecting...";
|
||||
|
||||
try
|
||||
{
|
||||
var address = ServerAddressField.Text?.Trim();
|
||||
var username = UsernameField.Text?.Trim();
|
||||
var password = PasswordField.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
ShowError("Please enter a server address.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
ShowError("Please enter username and password.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(address, UriKind.Absolute, out var uri))
|
||||
uri = new Uri($"http://{address}");
|
||||
|
||||
await _connectionClient.UpdateServerAddressAsync(uri);
|
||||
await _authClient.AuthenticateAsync(username, password, uri);
|
||||
|
||||
IsAuthenticated = true;
|
||||
Close(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConnectButton.IsEnabled = true;
|
||||
ConnectButton.Content = "Connect";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisconnectClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_settingsProvider.Update(s =>
|
||||
{
|
||||
s.Authentication.Token = null;
|
||||
s.Authentication.ServerAddress = null;
|
||||
});
|
||||
|
||||
IsAuthenticated = false;
|
||||
DisconnectButton.IsVisible = false;
|
||||
Close(false);
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
ErrorLabel.Text = message;
|
||||
ErrorLabel.IsVisible = true;
|
||||
}
|
||||
}
|
||||
68
LANCommander.Packager/Views/MetadataSearchDialog.axaml
Normal file
68
LANCommander.Packager/Views/MetadataSearchDialog.axaml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LANCommander.Packager.Views.MetadataSearchDialog"
|
||||
Title="Metadata Lookup"
|
||||
Width="650" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource BackgroundBrush}">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="20">
|
||||
<!-- Search controls -->
|
||||
<StackPanel Grid.Row="0" Spacing="12" Margin="0,0,0,16">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<ComboBox Name="ProviderCombo" HorizontalAlignment="Stretch" MinWidth="200" />
|
||||
<ComboBox Name="SubProviderCombo" Width="180"
|
||||
IsVisible="False" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBox Name="SearchField" Watermark="Search for a game..."
|
||||
KeyDown="OnSearchKeyDown" MinWidth="400" />
|
||||
<Button Name="SearchButton" Content="Search" Classes="Primary"
|
||||
Padding="16,6" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Results -->
|
||||
<Border Grid.Row="1" BorderBrush="#2A2A2A" BorderThickness="1" CornerRadius="4">
|
||||
<Grid>
|
||||
<ListBox Name="ResultsList" Background="Transparent"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="4,6">
|
||||
<TextBlock Text="{Binding Title}" FontSize="14"
|
||||
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding ReleasedOn, StringFormat='{}{0:yyyy-MM-dd}'}"
|
||||
Opacity="0.5" FontSize="12"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Name="EmptyLabel" Text="Search for a game to see results"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Opacity="0.4" IsVisible="True" />
|
||||
|
||||
<StackPanel Name="LoadingPanel" Orientation="Horizontal" Spacing="8"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="Searching..." Opacity="0.5" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Pagination and actions -->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8">
|
||||
<Button Name="PrevButton" Content="Previous" IsVisible="False" />
|
||||
<Button Name="MoreButton" Content="Load More" IsVisible="False" />
|
||||
</StackPanel>
|
||||
<Button Name="CancelDialogButton" Content="Cancel" Grid.Column="2" Margin="0,0,8,0" />
|
||||
<Button Name="SelectButton" Content="Select" Grid.Column="3" Classes="Primary"
|
||||
IsEnabled="False" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
224
LANCommander.Packager/Views/MetadataSearchDialog.axaml.cs
Normal file
224
LANCommander.Packager/Views/MetadataSearchDialog.axaml.cs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using LANCommander.SDK.Models;
|
||||
using LANCommander.SDK.Services;
|
||||
using Game = LANCommander.SDK.Models.Manifest.Game;
|
||||
using Key = Avalonia.Input.Key;
|
||||
|
||||
namespace LANCommander.Packager.Views;
|
||||
|
||||
public partial class MetadataSearchDialog : Window
|
||||
{
|
||||
private readonly MetadataClient _metadataClient;
|
||||
private readonly ObservableCollection<Game> _results = new();
|
||||
private readonly List<MetadataSearchResult> _rawResults = new();
|
||||
private string? _selectedProvider;
|
||||
private int _offset;
|
||||
private bool _hasMore;
|
||||
|
||||
public Game? SelectedGame { get; private set; }
|
||||
|
||||
public MetadataSearchDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public MetadataSearchDialog(MetadataClient metadataClient, string? defaultSearch = null)
|
||||
{
|
||||
_metadataClient = metadataClient;
|
||||
InitializeComponent();
|
||||
|
||||
ResultsList.ItemsSource = _results;
|
||||
ResultsList.SelectionChanged += (_, _) =>
|
||||
SelectButton.IsEnabled = ResultsList.SelectedItem != null;
|
||||
|
||||
SearchButton.Click += async (_, _) => await SearchAsync();
|
||||
SelectButton.Click += OnSelectClick;
|
||||
CancelDialogButton.Click += (_, _) => Close(null);
|
||||
MoreButton.Click += async (_, _) => await LoadMoreAsync();
|
||||
|
||||
ProviderCombo.SelectionChanged += async (_, _) =>
|
||||
{
|
||||
_selectedProvider = ProviderCombo.SelectedItem as string;
|
||||
if (_selectedProvider != null)
|
||||
await LoadSubProvidersAsync(_selectedProvider);
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(defaultSearch))
|
||||
SearchField.Text = defaultSearch;
|
||||
|
||||
_ = LoadProvidersAsync();
|
||||
}
|
||||
|
||||
private async Task LoadProvidersAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var providers = await _metadataClient.GetProvidersAsync();
|
||||
var list = providers.ToList();
|
||||
|
||||
ProviderCombo.ItemsSource = list;
|
||||
|
||||
if (list.Count > 0)
|
||||
ProviderCombo.SelectedIndex = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmptyLabel.Text = $"Failed to load providers: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSubProvidersAsync(string provider)
|
||||
{
|
||||
try
|
||||
{
|
||||
var subProviders = await _metadataClient.GetSubProvidersAsync(provider);
|
||||
var list = subProviders?.ToList();
|
||||
|
||||
if (list != null && list.Count > 0)
|
||||
{
|
||||
SubProviderCombo.ItemsSource = list.Select(sp => sp.Name).ToList();
|
||||
SubProviderCombo.IsVisible = true;
|
||||
SubProviderCombo.SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
SubProviderCombo.IsVisible = false;
|
||||
SubProviderCombo.SelectedItem = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
SubProviderCombo.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
_ = SearchAsync();
|
||||
}
|
||||
|
||||
private async Task SearchAsync()
|
||||
{
|
||||
var query = SearchField.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(query) || _selectedProvider == null)
|
||||
return;
|
||||
|
||||
_offset = 0;
|
||||
_results.Clear();
|
||||
_rawResults.Clear();
|
||||
|
||||
LoadingPanel.IsVisible = true;
|
||||
EmptyLabel.IsVisible = false;
|
||||
SearchButton.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
var subProvider = SubProviderCombo.IsVisible
|
||||
? SubProviderCombo.SelectedItem as string
|
||||
: null;
|
||||
|
||||
var results = await _metadataClient.SearchAsync(_selectedProvider, query, subProvider, 10, 0);
|
||||
|
||||
if (results?.Results != null)
|
||||
{
|
||||
foreach (var result in results.Results)
|
||||
{
|
||||
_rawResults.Add(result);
|
||||
_results.Add(result.Data);
|
||||
}
|
||||
|
||||
_hasMore = results.More;
|
||||
_offset = results.Offset + results.Limit;
|
||||
MoreButton.IsVisible = _hasMore;
|
||||
}
|
||||
|
||||
if (_results.Count == 0)
|
||||
EmptyLabel.Text = "No results found.";
|
||||
|
||||
EmptyLabel.IsVisible = _results.Count == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmptyLabel.Text = $"Search failed: {ex.Message}";
|
||||
EmptyLabel.IsVisible = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingPanel.IsVisible = false;
|
||||
SearchButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMoreAsync()
|
||||
{
|
||||
if (_selectedProvider == null)
|
||||
return;
|
||||
|
||||
var query = SearchField.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return;
|
||||
|
||||
MoreButton.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
var subProvider = SubProviderCombo.IsVisible
|
||||
? SubProviderCombo.SelectedItem as string
|
||||
: null;
|
||||
|
||||
var results = await _metadataClient.SearchAsync(_selectedProvider, query, subProvider, 10, _offset);
|
||||
|
||||
if (results?.Results != null)
|
||||
{
|
||||
foreach (var result in results.Results)
|
||||
{
|
||||
_rawResults.Add(result);
|
||||
_results.Add(result.Data);
|
||||
}
|
||||
|
||||
_hasMore = results.More;
|
||||
_offset = results.Offset + results.Limit;
|
||||
}
|
||||
|
||||
MoreButton.IsVisible = _hasMore;
|
||||
}
|
||||
catch
|
||||
{
|
||||
MoreButton.IsVisible = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
MoreButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSelectClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedIndex = ResultsList.SelectedIndex;
|
||||
if (selectedIndex < 0 || selectedIndex >= _rawResults.Count || _selectedProvider == null)
|
||||
return;
|
||||
|
||||
SelectButton.IsEnabled = false;
|
||||
SelectButton.Content = "Loading...";
|
||||
|
||||
try
|
||||
{
|
||||
var rawResult = _rawResults[selectedIndex];
|
||||
var fullGame = await _metadataClient.GetGameAsync(_selectedProvider, rawResult.Id);
|
||||
|
||||
SelectedGame = fullGame;
|
||||
Close(fullGame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmptyLabel.Text = $"Failed to load game details: {ex.Message}";
|
||||
EmptyLabel.IsVisible = true;
|
||||
SelectButton.IsEnabled = true;
|
||||
SelectButton.Content = "Select";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
x:Class="LANCommander.Packager.Views.MetadataView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12" MaxWidth="500">
|
||||
<Button Name="LookupButton" Content="Lookup Metadata..." IsEnabled="False"
|
||||
HorizontalAlignment="Left" Padding="12,6" Margin="0,0,0,4" />
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Title *" Opacity="0.7" />
|
||||
<TextBox Name="TitleField" />
|
||||
|
|
|
|||
|
|
@ -1,22 +1,106 @@
|
|||
using Avalonia.Controls;
|
||||
using LANCommander.Packager.Models;
|
||||
using LANCommander.SDK.Models.Manifest;
|
||||
using LANCommander.SDK.Services;
|
||||
|
||||
namespace LANCommander.Packager.Views;
|
||||
|
||||
public partial class MetadataView : UserControl
|
||||
{
|
||||
private readonly PackageContext _context;
|
||||
private readonly MetadataClient? _metadataClient;
|
||||
|
||||
public MetadataView(PackageContext context)
|
||||
public MetadataView(PackageContext context) : this(context, null) { }
|
||||
|
||||
public MetadataView(PackageContext context, MetadataClient? metadataClient)
|
||||
{
|
||||
_context = context;
|
||||
_metadataClient = metadataClient;
|
||||
InitializeComponent();
|
||||
ReleasedOnPicker.SelectedDate = DateTime.Today;
|
||||
|
||||
LookupButton.Click += async (_, _) => await OnLookupClick();
|
||||
}
|
||||
|
||||
public void SetAuthenticated(bool authenticated)
|
||||
{
|
||||
LookupButton.IsEnabled = authenticated && _metadataClient != null;
|
||||
}
|
||||
|
||||
public void SetDefaultTitle(string title)
|
||||
{
|
||||
TitleField.Text = title;
|
||||
if (string.IsNullOrWhiteSpace(TitleField.Text))
|
||||
TitleField.Text = title;
|
||||
}
|
||||
|
||||
private async Task OnLookupClick()
|
||||
{
|
||||
if (_metadataClient == null)
|
||||
return;
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is not Window window)
|
||||
return;
|
||||
|
||||
var dialog = new MetadataSearchDialog(_metadataClient, TitleField.Text);
|
||||
var result = await dialog.ShowDialog<Game?>(window);
|
||||
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
PopulateFromGame(result);
|
||||
}
|
||||
|
||||
private void PopulateFromGame(Game game)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(game.Title))
|
||||
TitleField.Text = game.Title;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(game.SortTitle))
|
||||
SortTitleField.Text = game.SortTitle;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(game.Description))
|
||||
DescriptionField.Text = game.Description;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(game.Notes))
|
||||
NotesField.Text = game.Notes;
|
||||
|
||||
if (game.ReleasedOn != default)
|
||||
ReleasedOnPicker.SelectedDate = game.ReleasedOn;
|
||||
|
||||
SingleplayerCheckbox.IsChecked = game.Singleplayer;
|
||||
|
||||
var manifest = _context.Manifest;
|
||||
|
||||
if (game.Genres.Count > 0)
|
||||
manifest.Genres = game.Genres;
|
||||
|
||||
if (game.Tags.Count > 0)
|
||||
manifest.Tags = game.Tags;
|
||||
|
||||
if (game.Developers.Count > 0)
|
||||
manifest.Developers = game.Developers;
|
||||
|
||||
if (game.Publishers.Count > 0)
|
||||
manifest.Publishers = game.Publishers;
|
||||
|
||||
if (game.Platforms.Count > 0)
|
||||
manifest.Platforms = game.Platforms;
|
||||
|
||||
if (game.MultiplayerModes.Count > 0)
|
||||
manifest.MultiplayerModes = game.MultiplayerModes;
|
||||
|
||||
if (game.Collections.Count > 0)
|
||||
manifest.Collections = game.Collections;
|
||||
|
||||
if (game.ExternalIds.Count > 0)
|
||||
manifest.ExternalIds = game.ExternalIds;
|
||||
|
||||
if (game.Engine != null)
|
||||
manifest.Engine = game.Engine;
|
||||
|
||||
if (game.Type != default)
|
||||
manifest.Type = game.Type;
|
||||
}
|
||||
|
||||
public void ApplyMetadata()
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LANCommander.Packager.Views.OutputView">
|
||||
<DockPanel>
|
||||
<!-- Generate button and progress pinned to bottom -->
|
||||
<!-- Generate button, upload button, and progress pinned to bottom -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Spacing="8">
|
||||
<Button Name="GenerateButton" Content="Generate .LCX" Classes="Primary"
|
||||
HorizontalAlignment="Center" Padding="32,12" FontSize="16"
|
||||
Margin="0,8" />
|
||||
|
||||
<ProgressBar Name="Progress" Minimum="0" Maximum="1" Value="0" Height="8"
|
||||
IsVisible="False" />
|
||||
|
||||
<TextBlock Name="StatusLabel" TextWrapping="Wrap" HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="12"
|
||||
Margin="0,8">
|
||||
<Button Name="GenerateButton" Content="Generate .LCX" Classes="Primary"
|
||||
Padding="32,12" FontSize="16" />
|
||||
<Button Name="UploadButton" Content="Upload to Server" Classes="Success"
|
||||
Padding="32,12" FontSize="16" IsVisible="False" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Scrollable content area -->
|
||||
|
|
|
|||
|
|
@ -5,16 +5,26 @@ using Avalonia.Platform.Storage;
|
|||
using Avalonia.Threading;
|
||||
using LANCommander.Packager.Models;
|
||||
using LANCommander.Packager.Services;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Factories;
|
||||
using LANCommander.SDK.Services;
|
||||
|
||||
namespace LANCommander.Packager.Views;
|
||||
|
||||
public partial class OutputView : UserControl
|
||||
{
|
||||
private readonly PackageContext _context;
|
||||
private readonly ApiRequestFactory? _apiRequestFactory;
|
||||
private readonly ISettingsProvider? _settingsProvider;
|
||||
private bool _packageGenerated;
|
||||
|
||||
public OutputView(PackageContext context)
|
||||
public OutputView(PackageContext context) : this(context, null, null) { }
|
||||
|
||||
public OutputView(PackageContext context, ApiRequestFactory? apiRequestFactory, ISettingsProvider? settingsProvider)
|
||||
{
|
||||
_context = context;
|
||||
_apiRequestFactory = apiRequestFactory;
|
||||
_settingsProvider = settingsProvider;
|
||||
InitializeComponent();
|
||||
|
||||
GenerateButton.Click += async (s, e) =>
|
||||
|
|
@ -22,6 +32,11 @@ public partial class OutputView : UserControl
|
|||
await GeneratePackageAsync();
|
||||
};
|
||||
|
||||
UploadButton.Click += async (s, e) =>
|
||||
{
|
||||
await UploadPackageAsync();
|
||||
};
|
||||
|
||||
BrowseButton.Click += async (s, e) =>
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
|
|
@ -40,6 +55,11 @@ public partial class OutputView : UserControl
|
|||
};
|
||||
}
|
||||
|
||||
public void SetAuthenticated(bool authenticated)
|
||||
{
|
||||
UploadButton.IsVisible = authenticated && _apiRequestFactory != null;
|
||||
}
|
||||
|
||||
public void SetDefaultOutputPath()
|
||||
{
|
||||
var title = _context.Manifest.Title ?? "Game";
|
||||
|
|
@ -75,8 +95,10 @@ public partial class OutputView : UserControl
|
|||
ApplyOptions();
|
||||
|
||||
GenerateButton.IsEnabled = false;
|
||||
UploadButton.IsEnabled = false;
|
||||
Progress.IsVisible = true;
|
||||
Progress.Value = 0;
|
||||
_packageGenerated = false;
|
||||
|
||||
var progress = new Progress<string>(message =>
|
||||
{
|
||||
|
|
@ -106,10 +128,13 @@ public partial class OutputView : UserControl
|
|||
if (_context.WriteSummaryLog)
|
||||
WriteSummaryLog(outputPath, sizeMb);
|
||||
|
||||
_packageGenerated = true;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusLabel.Text = $"Package created successfully!\n{outputPath}\nSize: {sizeMb:F2} MB";
|
||||
GenerateButton.IsEnabled = true;
|
||||
UploadButton.IsEnabled = true;
|
||||
Progress.IsVisible = false;
|
||||
});
|
||||
}
|
||||
|
|
@ -119,11 +144,99 @@ public partial class OutputView : UserControl
|
|||
{
|
||||
StatusLabel.Text = $"Error: {ex.Message}";
|
||||
GenerateButton.IsEnabled = true;
|
||||
UploadButton.IsEnabled = true;
|
||||
Progress.IsVisible = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UploadPackageAsync()
|
||||
{
|
||||
if (_apiRequestFactory == null || _settingsProvider == null)
|
||||
return;
|
||||
|
||||
var outputPath = _context.OutputPath;
|
||||
|
||||
if (!_packageGenerated || string.IsNullOrWhiteSpace(outputPath) || !File.Exists(outputPath))
|
||||
{
|
||||
await GeneratePackageAsync();
|
||||
|
||||
if (!_packageGenerated)
|
||||
return;
|
||||
|
||||
outputPath = _context.OutputPath;
|
||||
}
|
||||
|
||||
UploadButton.IsEnabled = false;
|
||||
GenerateButton.IsEnabled = false;
|
||||
Progress.IsVisible = true;
|
||||
Progress.Value = 0;
|
||||
|
||||
try
|
||||
{
|
||||
StatusLabel.Text = "Uploading package to server...";
|
||||
Progress.IsIndeterminate = true;
|
||||
|
||||
var chunkSize = _settingsProvider.CurrentValue.Archives.UploadChunkSize;
|
||||
|
||||
var objectKey = await Task.Run(async () =>
|
||||
{
|
||||
using var fs = new FileStream(outputPath, FileMode.Open, FileAccess.Read);
|
||||
|
||||
return await _apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UploadInChunksAsync(chunkSize, fs);
|
||||
});
|
||||
|
||||
if (objectKey == Guid.Empty)
|
||||
throw new Exception("Upload failed. Check that the server is reachable and you have permission to import games.");
|
||||
|
||||
Dispatcher.UIThread.Post(() => StatusLabel.Text = "Importing package on server...");
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Games/Import/{objectKey}")
|
||||
.PostAsync();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
File.Delete(outputPath);
|
||||
}
|
||||
catch { }
|
||||
|
||||
_packageGenerated = false;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusLabel.Text = "Package uploaded and imported successfully!";
|
||||
Progress.IsIndeterminate = false;
|
||||
Progress.Value = 1;
|
||||
Progress.IsVisible = false;
|
||||
UploadButton.IsEnabled = true;
|
||||
GenerateButton.IsEnabled = true;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusLabel.Text = $"Upload failed: {ex.Message}";
|
||||
Progress.IsIndeterminate = false;
|
||||
Progress.IsVisible = false;
|
||||
UploadButton.IsEnabled = true;
|
||||
GenerateButton.IsEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSummaryLog(string outputPath, double sizeMb)
|
||||
{
|
||||
var manifest = _context.Manifest;
|
||||
|
|
|
|||
57
LANCommander.SDK/Clients/MetadataClient.cs
Normal file
57
LANCommander.SDK/Clients/MetadataClient.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK.Factories;
|
||||
using LANCommander.SDK.Models;
|
||||
using Game = LANCommander.SDK.Models.Manifest.Game;
|
||||
|
||||
namespace LANCommander.SDK.Services;
|
||||
|
||||
public class MetadataClient(ApiRequestFactory apiRequestFactory)
|
||||
{
|
||||
public async Task<IEnumerable<string>> GetProvidersAsync()
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute("/api/Metadata/Providers")
|
||||
.GetAsync<IEnumerable<string>>();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MetadataSubProvider>> GetSubProvidersAsync(string provider)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Metadata/{provider}/SubProviders")
|
||||
.GetAsync<IEnumerable<MetadataSubProvider>>();
|
||||
}
|
||||
|
||||
public async Task<MetadataSearchResultsCollection> SearchAsync(
|
||||
string provider, string query, string? subProvider = null, int limit = 10, int offset = 0)
|
||||
{
|
||||
var route = $"/api/Metadata/{provider}/Search?query={Uri.EscapeDataString(query)}&limit={limit}&offset={offset}";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subProvider))
|
||||
route += $"&subProvider={Uri.EscapeDataString(subProvider)}";
|
||||
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute(route)
|
||||
.GetAsync<MetadataSearchResultsCollection>();
|
||||
}
|
||||
|
||||
public async Task<Game> GetGameAsync(string provider, string gameId)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Metadata/{provider}/{Uri.EscapeDataString(gameId)}")
|
||||
.GetAsync<Game>();
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ public static class IServiceCollectionExtensions
|
|||
services.AddSingleton<ServerClient>();
|
||||
services.AddSingleton<TagClient>();
|
||||
services.AddSingleton<ToolClient>();
|
||||
services.AddSingleton<MetadataClient>();
|
||||
|
||||
services.AddSingleton<MigrationHistoryService>();
|
||||
services.AddSingleton<MigrationService>();
|
||||
|
|
|
|||
23
LANCommander.SDK/Models/MetadataSearchResult.cs
Normal file
23
LANCommander.SDK/Models/MetadataSearchResult.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace LANCommander.SDK.Models;
|
||||
|
||||
public class MetadataSearchResult
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public Manifest.Game Data { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MetadataSearchResultsCollection
|
||||
{
|
||||
public ICollection<MetadataSearchResult> Results { get; set; } = new List<MetadataSearchResult>();
|
||||
public bool More { get; set; }
|
||||
public int Limit { get; set; }
|
||||
public int Offset { get; set; }
|
||||
}
|
||||
|
||||
public class MetadataSubProvider
|
||||
{
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
80
LANCommander.Server/Endpoints/MetadataEndpoints.cs
Normal file
80
LANCommander.Server/Endpoints/MetadataEndpoints.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using LANCommander.Server.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LANCommander.Server.Endpoints;
|
||||
|
||||
public static class MetadataEndpoints
|
||||
{
|
||||
public static void MapMetadataEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/Metadata").RequireAuthorization();
|
||||
|
||||
group.MapGet("/Providers", GetProvidersAsync);
|
||||
group.MapGet("/{provider}/SubProviders", GetSubProvidersAsync);
|
||||
group.MapGet("/{provider}/Search", SearchAsync);
|
||||
group.MapGet("/{provider}/{gameId}", GetGameAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetProvidersAsync(
|
||||
[FromServices] MetadataService metadataService)
|
||||
{
|
||||
var providers = metadataService.GetProviderNames();
|
||||
|
||||
return TypedResults.Ok(providers);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetSubProvidersAsync(
|
||||
string provider,
|
||||
[FromServices] MetadataService metadataService)
|
||||
{
|
||||
var metadataProvider = metadataService.GetProvider(provider);
|
||||
|
||||
if (metadataProvider == null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
var subProviders = await metadataProvider.GetSubProvidersAsync();
|
||||
|
||||
if (subProviders == null)
|
||||
return TypedResults.Ok(Array.Empty<object>());
|
||||
|
||||
return TypedResults.Ok(subProviders.Select(sp => new { sp.Slug, sp.Name }));
|
||||
}
|
||||
|
||||
private static async Task<IResult> SearchAsync(
|
||||
string provider,
|
||||
[FromQuery] string query,
|
||||
[FromQuery] string? subProvider,
|
||||
[FromQuery] int limit = 10,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromServices] MetadataService metadataService = default!)
|
||||
{
|
||||
var metadataProvider = metadataService.GetProvider(provider);
|
||||
|
||||
if (metadataProvider == null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
var results = string.IsNullOrWhiteSpace(subProvider)
|
||||
? await metadataProvider.SearchGamesAsync(query, limit, offset)
|
||||
: await metadataProvider.SearchGamesAsync(query, subProvider, limit, offset);
|
||||
|
||||
return TypedResults.Ok(results);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetGameAsync(
|
||||
string provider,
|
||||
string gameId,
|
||||
[FromServices] MetadataService metadataService)
|
||||
{
|
||||
var metadataProvider = metadataService.GetProvider(provider);
|
||||
|
||||
if (metadataProvider == null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
var game = await metadataProvider.GetGameAsync(gameId);
|
||||
|
||||
if (game == null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
return TypedResults.Ok(game);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ public static class Endpoints
|
|||
endpoints.MapSettingsEndpoints();
|
||||
endpoints.MapLogEndpoints();
|
||||
endpoints.MapHqEndpoints();
|
||||
endpoints.MapMetadataEndpoints();
|
||||
endpoints.MapTagEndpoints();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapFallbackToPage("/_Host");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue