diff --git a/src/StarBreaker.Cli/P4kCommands/ExtractP4kCommand.cs b/src/StarBreaker.Cli/P4kCommands/ExtractP4kCommand.cs index 827bdd5..8e3583d 100644 --- a/src/StarBreaker.Cli/P4kCommands/ExtractP4kCommand.cs +++ b/src/StarBreaker.Cli/P4kCommands/ExtractP4kCommand.cs @@ -22,7 +22,7 @@ public class ExtractP4kCommand : ICommand public ValueTask ExecuteAsync(IConsole console) { - var p4k = new P4kFile(P4kFile); + var p4k = P4k.P4kFile.FromFile(P4kFile); console.Output.WriteLine("DataForge loaded."); console.Output.WriteLine("Exporting..."); diff --git a/src/StarBreaker.P4k/P4kFile.cs b/src/StarBreaker.P4k/P4kFile.cs index 1512bac..7033283 100644 --- a/src/StarBreaker.P4k/P4kFile.cs +++ b/src/StarBreaker.P4k/P4kFile.cs @@ -15,10 +15,16 @@ public sealed class P4kFile public ZipEntry[] Entries => _entries; - public P4kFile(string filePath) + private P4kFile(string path, ZipEntry[] entries) { - P4KPath = filePath; - using var reader = new BinaryReader(new FileStream(P4KPath, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024), Encoding.UTF8, false); + P4KPath = path; + _entries = entries; + } + + public static P4kFile FromFile(string filePath, IProgress? progress = null) + { + progress?.Report(0); + using var reader = new BinaryReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024), Encoding.UTF8, false); var eocdLocation = reader.Locate(EOCDRecord.Magic); reader.BaseStream.Seek(eocdLocation, SeekOrigin.Begin); @@ -42,8 +48,8 @@ public P4kFile(string filePath) if (eocd64.Signature != BitConverter.ToUInt32(EOCD64Record.Magic)) throw new Exception("Invalid zip64 end of central directory locator"); - _entries = new ZipEntry[eocd64.EntriesOnDisk]; - + var _entries = new ZipEntry[eocd64.EntriesOnDisk]; + var reportInterval = (int)Math.Max(eocd64.TotalEntries / 50, 1); reader.BaseStream.Seek((long)eocd64.CentralDirectoryOffset, SeekOrigin.Begin); for (var i = 0; i < (int)eocd64.TotalEntries; i++) @@ -115,12 +121,19 @@ public P4kFile(string filePath) header.LastModifiedTime, header.LastModifiedDate ); + + if (i % reportInterval == 0) + progress?.Report(i / (double)eocd64.TotalEntries); } finally { ArrayPool.Shared.Return(rent); } } + + progress?.Report(1); + + return new P4kFile(filePath, _entries); } public void Extract(string outputDir, string? filter = null, IProgress? progress = null) @@ -132,7 +145,7 @@ public void Extract(string outputDir, string? filter = null, IProgress? var processedEntries = 0; progress?.Report(0); - + //TODO: Preprocessing step: // 1. start with the list of total files // 2. run the following according to the filter: diff --git a/src/StarBreaker.Sandbox/TimeP4kExtract.cs b/src/StarBreaker.Sandbox/TimeP4kExtract.cs index 2ee825a..0218183 100644 --- a/src/StarBreaker.Sandbox/TimeP4kExtract.cs +++ b/src/StarBreaker.Sandbox/TimeP4kExtract.cs @@ -10,7 +10,7 @@ public static class TimeP4kExtract public static void Run() { var sw1 = Stopwatch.StartNew(); - var p4kFile = new P4kFile(p4k); + var p4kFile = P4kFile.FromFile(p4k); sw1.Stop(); Console.WriteLine($"Took {sw1.ElapsedMilliseconds}ms to load {p4kFile.Entries.Length} entries"); diff --git a/src/StarBreaker.Sandbox/TimeZipNode.cs b/src/StarBreaker.Sandbox/TimeZipNode.cs index 02efaca..bf2d0f9 100644 --- a/src/StarBreaker.Sandbox/TimeZipNode.cs +++ b/src/StarBreaker.Sandbox/TimeZipNode.cs @@ -9,7 +9,7 @@ public static class TimeZipNode public static void Run() { - var p4kFile = new P4kFile(p4k); + var p4kFile = P4kFile.FromFile(p4k); var times = new List(); for (var i = 0; i < 8; i++) diff --git a/src/StarBreaker.slnx b/src/StarBreaker.slnx index dd878e1..c9f1845 100644 --- a/src/StarBreaker.slnx +++ b/src/StarBreaker.slnx @@ -16,4 +16,5 @@ + \ No newline at end of file diff --git a/src/StarBreaker/App.axaml b/src/StarBreaker/App.axaml new file mode 100644 index 0000000..aecd5e8 --- /dev/null +++ b/src/StarBreaker/App.axaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/StarBreaker/App.axaml.cs b/src/StarBreaker/App.axaml.cs new file mode 100644 index 0000000..4abe7af --- /dev/null +++ b/src/StarBreaker/App.axaml.cs @@ -0,0 +1,84 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; +using Avalonia.Styling; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using StarBreaker.Extensions; +using StarBreaker.Screens; +using StarBreaker.Services; + +namespace StarBreaker; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + if (Design.IsDesignMode) + { + RequestedThemeVariant = ThemeVariant.Dark; + } + } + + private SplashWindow? _splashWindow; + private MainWindow? _mainWindow; + + public override void OnFrameworkInitializationCompleted() + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT +#pragma warning disable IL2026 + BindingPlugins.DataValidators.RemoveAt(0); +#pragma warning restore IL2026 + + var collection = new ServiceCollection(); + + collection.RegisterServices(); + ViewLocator.RegisterViews(); + + Services = collection.BuildServiceProvider(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var splashVm = Services.GetRequiredService(); + _splashWindow = new SplashWindow { DataContext = splashVm }; + + splashVm.P4kLoaded += SwapWindows; + + desktop.MainWindow = _splashWindow; + _splashWindow.Show(); + } + + base.OnFrameworkInitializationCompleted(); + } + + private void SwapWindows(object? sender, EventArgs e) + { + if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) + return; + + var mainVm = Services.GetRequiredService(); + _mainWindow = new MainWindow { DataContext = mainVm }; + + //do not change the order of these + _mainWindow.Show(); + _splashWindow!.Close(); + + desktop.MainWindow = _mainWindow; + } + + public new static App Current => Application.Current as App ?? throw new InvalidOperationException("App.Current is null"); + + public static IStorageProvider StorageProvider => (Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow?.StorageProvider ?? + throw new InvalidOperationException("StorageProvider is null"); + + /// + /// Gets the instance to resolve application services. + /// + public IServiceProvider Services { get; private set; } = null!; +} \ No newline at end of file diff --git a/src/StarBreaker/Assets/avalonia-logo.ico b/src/StarBreaker/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/src/StarBreaker/Assets/avalonia-logo.ico differ diff --git a/src/StarBreaker/Constants.cs b/src/StarBreaker/Constants.cs new file mode 100644 index 0000000..27ce79b --- /dev/null +++ b/src/StarBreaker/Constants.cs @@ -0,0 +1,24 @@ +using Avalonia.Platform.Storage; + +namespace StarBreaker; + +public static class Constants +{ + public const string DefaultStarCitizenFolder = @"C:\Program Files\Roberts Space Industries\StarCitizen\"; + public const string DataP4k = "Data.p4k"; + + public static FilePickerOpenOptions GetP4kFilter(IStorageFolder? defaultPath) => new() + { + FileTypeFilter = + [ + new FilePickerFileType("P4k File") + { + Patterns = ["*.p4k"], + } + ], + AllowMultiple = false, + Title = "Select a P4k file", + SuggestedFileName = DataP4k, + SuggestedStartLocation = defaultPath + }; +} \ No newline at end of file diff --git a/src/StarBreaker/DesignData.cs b/src/StarBreaker/DesignData.cs new file mode 100644 index 0000000..2db1663 --- /dev/null +++ b/src/StarBreaker/DesignData.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using StarBreaker.Screens; +using StarBreaker.Services; + +namespace StarBreaker; + +public static class DesignData +{ + public static SplashWindowViewModel SplashWindowViewModel { get; } = App.Current.Services.GetRequiredService(); + public static MainWindowViewModel MainWindowViewModel { get; } = App.Current.Services.GetRequiredService(); + public static HomeViewModel HomeViewModel { get; } = App.Current.Services.GetRequiredService(); +} \ No newline at end of file diff --git a/src/StarBreaker/Extensions/AppWindowExtensions.cs b/src/StarBreaker/Extensions/AppWindowExtensions.cs new file mode 100644 index 0000000..02d0c1c --- /dev/null +++ b/src/StarBreaker/Extensions/AppWindowExtensions.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using Avalonia.Media; +using FluentAvalonia.UI.Windowing; + +namespace StarBreaker.Extensions; + +public static class AppWindowExtensions +{ + public static void EnableMicaTransparency(this AppWindow window) + { + window.TransparencyLevelHint = [WindowTransparencyLevel.Mica]; + window.Background = new SolidColorBrush(new Color(0, 0,0,0)); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Extensions/ServiceCollectionExtensions.cs b/src/StarBreaker/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..42cd69c --- /dev/null +++ b/src/StarBreaker/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using StarBreaker.Screens; +using StarBreaker.Services; + +namespace StarBreaker.Extensions; + +public static class ServiceCollectionExtensions +{ + public static void RegisterServices(this ServiceCollection services) + { + services.AddLogging(b => { b.AddSimpleConsole(options => { options.SingleLine = true; }); }); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Models/ZipNode.cs b/src/StarBreaker/Models/ZipNode.cs new file mode 100644 index 0000000..5cc35d3 --- /dev/null +++ b/src/StarBreaker/Models/ZipNode.cs @@ -0,0 +1,86 @@ +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; +using Humanizer; +using StarBreaker.P4k; +using StarBreaker.Screens; + +namespace StarBreaker.Models; + +public partial class ZipNode : ViewModelBase +{ + private static readonly Dictionary EmptyList = new(); + + public ZipEntry? ZipEntry { get; } + public string Name { get; } + public Dictionary Children { get; } + + /// + /// Constructor for creating a file node + /// + /// + public ZipNode(ZipEntry entry) + { + ZipEntry = entry; + Name = entry.Name.Split('\\').Last(); + Children = EmptyList; + } + + /// + /// Constructor for creating a directory node + /// + /// + public ZipNode(string name) + { + Name = name; + Children = new Dictionary(); + } + + public string SizeUi => ((long?)ZipEntry?.UncompressedSize)?.Bytes().ToString() ?? ""; + public string DateModifiedUi => ZipEntry?.LastModified.ToString("yyyy-mm-dd", CultureInfo.InvariantCulture) ?? ""; + public string CompressionMethodUi => ZipEntry?.CompressionMethod.ToString() ?? ""; + public string EncryptedUi => ZipEntry?.IsCrypted.ToString() ?? ""; + + [ObservableProperty] + private bool _isChecked; + + public ZipNode(ZipEntry[] zipEntries, IProgress? progress = null) + { + progress?.Report(0); + var report = Math.Max(1, zipEntries.Length / 100); + Name = ""; + var root = new ZipNode(""); + int count = 0; + foreach (var zipEntry in zipEntries) + { + var parts = zipEntry.Name.Split('\\'); + var current = root; + + for (var index = 0; index < parts.Length; index++) + { + var part = parts[index]; + + // If this is the last part, we're at the file + if (index == parts.Length - 1) + { + current.Children[part] = new ZipNode(zipEntry); + break; + } + + if (!current.Children.TryGetValue(part, out var value)) + { + value = new ZipNode(part); + current.Children[part] = value; + } + + current = value; + } + count++; + if (progress != null && count % report == 0) + { + progress.Report(count / (double)zipEntries.Count()); + } + } + + Children = root.Children; + } +} \ No newline at end of file diff --git a/src/StarBreaker/Program.cs b/src/StarBreaker/Program.cs new file mode 100644 index 0000000..117e0d8 --- /dev/null +++ b/src/StarBreaker/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; + +namespace StarBreaker; + +public static class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/IPageViewModel.cs b/src/StarBreaker/Screens/IPageViewModel.cs new file mode 100644 index 0000000..f08349d --- /dev/null +++ b/src/StarBreaker/Screens/IPageViewModel.cs @@ -0,0 +1,7 @@ +namespace StarBreaker.Screens; + +public interface IPageViewModel +{ + string Name { get; } + string Icon { get; } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/MainWindow/MainWindow.axaml b/src/StarBreaker/Screens/MainWindow/MainWindow.axaml new file mode 100644 index 0000000..5b2e2a0 --- /dev/null +++ b/src/StarBreaker/Screens/MainWindow/MainWindow.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/StarBreaker/Screens/MainWindow/MainWindow.axaml.cs b/src/StarBreaker/Screens/MainWindow/MainWindow.axaml.cs new file mode 100644 index 0000000..26d0668 --- /dev/null +++ b/src/StarBreaker/Screens/MainWindow/MainWindow.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; +using Avalonia.Media; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Windowing; +using StarBreaker.Extensions; + +namespace StarBreaker.Screens; + +public partial class MainWindow : AppWindow +{ + public MainWindow() + { + InitializeComponent(); + this.EnableMicaTransparency(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/MainWindow/MainWindowViewModel.cs b/src/StarBreaker/Screens/MainWindow/MainWindowViewModel.cs new file mode 100644 index 0000000..60c83f0 --- /dev/null +++ b/src/StarBreaker/Screens/MainWindow/MainWindowViewModel.cs @@ -0,0 +1,28 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using StarBreaker.Services; + +namespace StarBreaker.Screens; + +public partial class MainWindowViewModel : ViewModelBase +{ + public MainWindowViewModel() + { + Pages = + [ + App.Current.Services.GetRequiredService(), + App.Current.Services.GetRequiredService(), + ]; + + CurrentPage = _pages.First(); + } + + [ObservableProperty] + private IPageViewModel _currentPage; + + [ObservableProperty] + private ObservableCollection _pages; +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/PageViewModelBase.cs b/src/StarBreaker/Screens/PageViewModelBase.cs new file mode 100644 index 0000000..fbe5759 --- /dev/null +++ b/src/StarBreaker/Screens/PageViewModelBase.cs @@ -0,0 +1,7 @@ +namespace StarBreaker.Screens; + +public abstract class PageViewModelBase : ViewModelBase, IPageViewModel +{ + public abstract string Name { get; } + public abstract string Icon { get; } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml b/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml new file mode 100644 index 0000000..b1fb8c9 --- /dev/null +++ b/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml.cs b/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml.cs new file mode 100644 index 0000000..1a661e6 --- /dev/null +++ b/src/StarBreaker/Screens/SplashWindow/SplashWindow.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using FluentAvalonia.UI.Windowing; +using StarBreaker.Extensions; + +namespace StarBreaker.Screens; + +public partial class SplashWindow : AppWindow +{ + public SplashWindow() + { + InitializeComponent(); + this.EnableMicaTransparency(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/SplashWindow/SplashWindowViewModel.cs b/src/StarBreaker/Screens/SplashWindow/SplashWindowViewModel.cs new file mode 100644 index 0000000..54e067d --- /dev/null +++ b/src/StarBreaker/Screens/SplashWindow/SplashWindowViewModel.cs @@ -0,0 +1,103 @@ +using System.Collections.ObjectModel; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using StarBreaker.Services; + +namespace StarBreaker.Screens; + +public sealed partial class SplashWindowViewModel : ViewModelBase +{ + private readonly ILogger _logger; + private readonly IP4kService _p4kService; + + [ObservableProperty] private ObservableCollection _installations; + [ObservableProperty] private double? _progress; + + public SplashWindowViewModel(ILogger logger, IP4kService p4kService) + { + _logger = logger; + _p4kService = p4kService; + _installations = []; + Progress = null; + LoadDefaultP4kLocations(); + } + + public event EventHandler? P4kLoaded; + + private void OnP4kLoaded() + { + P4kLoaded?.Invoke(this, EventArgs.Empty); + } + + private async Task LoadP4k(string path) + { + Progress = 0; + var progress = new Progress(x => Dispatcher.UIThread.Post(() => Progress = x)); + + await Task.Run(() => _p4kService.OpenP4k(path, progress)); + + OnP4kLoaded(); + } + + [RelayCommand] + public async Task PickP4k() + { + _logger?.LogTrace("PickP4k enter"); + var defaultPath = await App.StorageProvider.TryGetFolderFromPathAsync(Constants.DefaultStarCitizenFolder); + var task = App.StorageProvider.OpenFilePickerAsync(Constants.GetP4kFilter(defaultPath)); + var file = await task; + if (file.Count != 1) + { + _logger?.LogError("OpenFilePickerAsync returned {Count} files", file.Count); + return; + } + + _logger?.LogTrace("PickP4k exit: {Path}", file[0].Path); + + await LoadP4k(file[0].Path.LocalPath); + } + + public void LoadDefaultP4kLocations() + { + var p4ks = Directory.GetFiles(Constants.DefaultStarCitizenFolder, Constants.DataP4k, SearchOption.AllDirectories); + if (p4ks.Length == 0) + { + _logger.LogError("No Data.p4k files found"); + return; + } + + _logger.LogTrace("Found {Count} Data.p4k files", p4ks.Length); + + foreach (var p4k in p4ks) + { + var directoryName = Path.GetDirectoryName(p4k); + if (directoryName is null) + { + _logger.LogError("Failed to get directory name for {Path}", p4k); + continue; + } + + Installations.Add(new StarCitizenInstallationViewModel + { + ChannelName = new DirectoryInfo(directoryName).Name, + Path = p4k + }); + } + } + + [RelayCommand] + public async Task ClickP4kLocation(string file) + { + _logger.LogTrace("ClickP4kLocation {Path}", file); + await LoadP4k(file); + } +} + +public sealed class StarCitizenInstallationViewModel +{ + public required string ChannelName { get; init; } + public required string Path { get; init; } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml b/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml new file mode 100644 index 0000000..f3412e2 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml @@ -0,0 +1,8 @@ + + ABOUT VIEW + diff --git a/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml.cs b/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml.cs new file mode 100644 index 0000000..e8af583 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/AboutView/AboutView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace StarBreaker.Screens; + +public partial class AboutView : UserControl +{ + public AboutView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/Tabs/AboutView/AboutViewModel.cs b/src/StarBreaker/Screens/Tabs/AboutView/AboutViewModel.cs new file mode 100644 index 0000000..e014945 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/AboutView/AboutViewModel.cs @@ -0,0 +1,7 @@ +namespace StarBreaker.Screens; + +public sealed class AboutViewModel : PageViewModelBase +{ + public override string Name => "About"; + public override string Icon => "Info"; +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml b/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml new file mode 100644 index 0000000..34913e9 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml.cs b/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml.cs new file mode 100644 index 0000000..c045817 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/HomeView/HomeView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace StarBreaker.Screens; + +public partial class HomeView : UserControl +{ + public HomeView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/Tabs/HomeView/HomeViewModel.cs b/src/StarBreaker/Screens/Tabs/HomeView/HomeViewModel.cs new file mode 100644 index 0000000..70f8902 --- /dev/null +++ b/src/StarBreaker/Screens/Tabs/HomeView/HomeViewModel.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using StarBreaker.Models; +using StarBreaker.Services; + +namespace StarBreaker.Screens; + +public sealed partial class HomeViewModel : PageViewModelBase +{ + public override string Name => "Home"; + public override string Icon => "Home"; + + private readonly IP4kService _p4KService; + + public HomeViewModel(IP4kService p4kService) + { + _p4KService = p4kService; + Source = new HierarchicalTreeDataGridSource(Array.Empty()) + { + Columns = + { + new CheckBoxColumn( + null, + x => x.IsChecked, + (o, v) => o.IsChecked = v, + options: new CheckBoxColumnOptions() + { + CanUserResizeColumn = false + } + ), + new HierarchicalExpanderColumn( + new TextColumn("File Name", x => x.Name, options: new TextColumnOptions() + { + IsTextSearchEnabled = true + }), + x => x.Children.Values + ), + new TextColumn("Size", x => x.SizeUi), + new TextColumn("Date", x => x.DateModifiedUi), + // new TextColumn("Compression", x => x.CompressionMethodUi), + // new TextColumn("Encrypted", x => x.EncryptedUi) + }, + }; + + Initialize(); + } + + [ObservableProperty] private HierarchicalTreeDataGridSource _source; + + [ObservableProperty] private double? _progress; + + private void Initialize() + { + if (_p4KService.P4kFile == null) + throw new InvalidOperationException("P4K file is not loaded"); + + Progress = 0.0f; + + Task.Run(() => + { + var progress = new Progress(value => Dispatcher.UIThread.InvokeAsync(() => Progress = (float)value)); + var zipFileEntries = new ZipNode(_p4KService.P4kFile.Entries, progress); + Dispatcher.UIThread.InvokeAsync(() => + { + Source.Items = zipFileEntries.Children.Values; + Progress = null; + }); + }); + } +} \ No newline at end of file diff --git a/src/StarBreaker/Screens/ViewModelBase.cs b/src/StarBreaker/Screens/ViewModelBase.cs new file mode 100644 index 0000000..04c5723 --- /dev/null +++ b/src/StarBreaker/Screens/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace StarBreaker.Screens; + +public class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/src/StarBreaker/Services/P4kService.cs b/src/StarBreaker/Services/P4kService.cs new file mode 100644 index 0000000..3086ce2 --- /dev/null +++ b/src/StarBreaker/Services/P4kService.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using StarBreaker.P4k; + +namespace StarBreaker.Services; + +public interface IP4kService +{ + P4kFile? P4kFile { get; } + void OpenP4k(string path, IProgress progress); +} + +public class P4kService : IP4kService +{ + private readonly ILogger _logger; + + public P4kFile? P4kFile { get; private set; } + + public P4kService(ILogger logger) + { + _logger = logger; + } + + public void OpenP4k(string path, IProgress progress) + { + if (P4kFile != null) + { + _logger.LogWarning("P4k file already open"); + return; + } + P4kFile = P4kFile.FromFile(path, progress); + } + + public int FileCount => P4kFile?.Entries.Length ?? 0; +} \ No newline at end of file diff --git a/src/StarBreaker/StarBreaker.csproj b/src/StarBreaker/StarBreaker.csproj new file mode 100644 index 0000000..178575b --- /dev/null +++ b/src/StarBreaker/StarBreaker.csproj @@ -0,0 +1,40 @@ + + + WinExe + true + app.manifest + true + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/StarBreaker/ViewLocator.cs b/src/StarBreaker/ViewLocator.cs new file mode 100644 index 0000000..147b527 --- /dev/null +++ b/src/StarBreaker/ViewLocator.cs @@ -0,0 +1,46 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using StarBreaker.Screens; + +namespace StarBreaker; + +public class ViewLocator : IDataTemplate +{ + public static void RegisterViews() + { + Register(); + Register(); + Register(); + Register(); + } + + private static readonly Dictionary> Registration = new(); + + public static void Register() where TView : Control, new() where TViewModel : ViewModelBase + { + Registration.Add(typeof(TViewModel), () => new TView()); + } + + public Control Build(object? data) + { + var type = data?.GetType(); + if (type == null) + { + return new TextBlock { Text = "Null" }; + } + + if (Registration.TryGetValue(type, out var factory)) + { + return factory(); + } + else + { + return new TextBlock { Text = "Not Found: " + type }; + } + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/src/StarBreaker/app.manifest b/src/StarBreaker/app.manifest new file mode 100644 index 0000000..9ba6465 --- /dev/null +++ b/src/StarBreaker/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + +