From c29658e7e4f5c3f9d7c96820de152ca40223cf64 Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 05:10:44 +0500 Subject: [PATCH 01/11] add storage functionality and appdata model --- .../Intefaces/IStorageService.cs | 9 +++++ MetaforceInstaller.Core/Models/AppData.cs | 6 ++++ .../Services/StorageService.cs | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 MetaforceInstaller.Core/Intefaces/IStorageService.cs create mode 100644 MetaforceInstaller.Core/Models/AppData.cs create mode 100644 MetaforceInstaller.Core/Services/StorageService.cs diff --git a/MetaforceInstaller.Core/Intefaces/IStorageService.cs b/MetaforceInstaller.Core/Intefaces/IStorageService.cs new file mode 100644 index 0000000..5967309 --- /dev/null +++ b/MetaforceInstaller.Core/Intefaces/IStorageService.cs @@ -0,0 +1,9 @@ +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Intefaces; + +public interface IStorageService +{ + AppData Load(); + void Save(AppData data); +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/AppData.cs b/MetaforceInstaller.Core/Models/AppData.cs new file mode 100644 index 0000000..5c37871 --- /dev/null +++ b/MetaforceInstaller.Core/Models/AppData.cs @@ -0,0 +1,6 @@ +namespace MetaforceInstaller.Core.Models; + +public class AppData +{ + public List Installations { get; set; } = new(); +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/StorageService.cs b/MetaforceInstaller.Core/Services/StorageService.cs new file mode 100644 index 0000000..7fa1601 --- /dev/null +++ b/MetaforceInstaller.Core/Services/StorageService.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using MetaforceInstaller.Core.Intefaces; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public class StorageService : IStorageService +{ + private readonly string _storagePath; + + public StorageService() + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDirectory = Path.Combine(appData, "MetaforceInstaller"); + Directory.CreateDirectory(appDirectory); + _storagePath = Path.Combine(appDirectory, "installations.json"); + } + + public AppData Load() + { + if (!File.Exists(_storagePath)) + return new AppData(); + + var json = File.ReadAllText(_storagePath); + return JsonSerializer.Deserialize(json) ?? new AppData(); + } + + public void Save(AppData data) + { + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(_storagePath, json); + } +} \ No newline at end of file From 3dc9c8c27997d68093c110484ac44436ab143a48 Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 05:11:05 +0500 Subject: [PATCH 02/11] split installation info to parts --- .../Models/InstallationData.cs | 10 +++ .../Models/InstallationParts.cs | 12 +++ .../Services/ZipScrapper.cs | 89 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 MetaforceInstaller.Core/Models/InstallationData.cs create mode 100644 MetaforceInstaller.Core/Models/InstallationParts.cs create mode 100644 MetaforceInstaller.Core/Services/ZipScrapper.cs diff --git a/MetaforceInstaller.Core/Models/InstallationData.cs b/MetaforceInstaller.Core/Models/InstallationData.cs new file mode 100644 index 0000000..855fae7 --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationData.cs @@ -0,0 +1,10 @@ +namespace MetaforceInstaller.Core.Models; + +public class InstallationData +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Title { get; set; } + public string AndroidPackagePath { get; set; } + public string WindowsServerPackagePath { get; set; } + public string WindowsAdminPackagePath { get; set; } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationParts.cs b/MetaforceInstaller.Core/Models/InstallationParts.cs new file mode 100644 index 0000000..cb531ab --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -0,0 +1,12 @@ +namespace MetaforceInstaller.Core.Models; + +public record InstallationParts +{ + public bool OculusClientExists { get; init; } + public bool PicoClientExists { get; init; } + public bool AndroidAdminExists { get; init; } + public bool AndroidContentExists { get; init; } + public bool WindowsContentExists { get; init; } + public bool WindowsAdminExists { get; init; } + public bool WindowsServerExists { get; init; } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs new file mode 100644 index 0000000..b5d1a32 --- /dev/null +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -0,0 +1,89 @@ +using System.IO.Compression; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public class ZipScrapper +{ + public static InstallationParts PeekFiles(ZipArchive archive) + { + return new InstallationParts + { + AndroidContentExists = DoesAndroidContentExists(archive), + OculusClientExists = DoesOculusClientExists(archive), + PicoClientExists = DoesPicoClientExists(archive), + AndroidAdminExists = DoesAndroidAdminExists(archive), + WindowsAdminExists = DoesPcAdminExists(archive), + WindowsContentExists = DoesWindowsContentExists(archive), + WindowsServerExists = DoesServerExists(archive), + }; + // Console.WriteLine($"Contents of {archive}:"); + // Console.WriteLine("----------------------------------"); + // + // foreach (ZipArchiveEntry entry in archive.Entries) + // { + // // You can access properties like entry.Name, entry.FullName, entry.Length (uncompressed size), entry.CompressedLength + // Console.WriteLine($"Entry: {entry.FullName}"); + // Console.WriteLine($" Uncompressed Size: {entry.Length} bytes"); + // Console.WriteLine($" Compressed Size: {entry.CompressedLength} bytes"); + // Console.WriteLine("----------------------------------"); + // } + } + + public static string ExtractZip(ZipArchive archive, string outputPath) + { + archive.ExtractToDirectory(outputPath); + return outputPath; + } + + private static bool DoesPicoClientExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.Name.Contains("MetaforcePico") + && entry.Name.EndsWith(".apk")); + } + + private static bool DoesOculusClientExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.Name.Contains("MetaforceOculus") + && entry.Name.EndsWith(".apk")); + } + + private static bool DoesAndroidAdminExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.Name.Contains("MetaforceAdmin") + && entry.Name.EndsWith(".apk")); + } + + private static bool DoesAndroidContentExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.Name.Contains("Content_Android") + && entry.Name.EndsWith(".zip")); + } + + private static bool DoesWindowsContentExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.Name.Contains("Content_StandaloneWindows") + && entry.Name.EndsWith(".zip")); + } + + private static bool DoesPcAdminExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.FullName.Contains("MetaforceAdminPC") + && entry.Name.EndsWith(".exe") && !entry.Name.Contains("UnityCrashHandler") && + !entry.Name.Contains("crashpad_handler")); + } + + private static bool DoesServerExists(ZipArchive archive) + { + return archive.Entries + .Any(entry => entry.FullName.Contains("MetaforceServer") + && entry.Name.EndsWith(".exe") && !entry.Name.Contains("UnityCrashHandler") && + !entry.Name.Contains("crashpad_handler")); + } +} \ No newline at end of file From 13a076ad791957cd1bf8ce15330425dda2c0ca15 Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 05:11:23 +0500 Subject: [PATCH 03/11] brbrbr patapim --- MetaforceInstaller.UI/MainWindow.axaml | 75 ++-- MetaforceInstaller.UI/MainWindow.axaml.cs | 398 +++++++++--------- .../NewInstallationDialog.axaml | 24 ++ .../NewInstallationDialog.axaml.cs | 118 ++++++ MetaforceInstaller.UI/TextDefaults.cs | 9 + 5 files changed, 388 insertions(+), 236 deletions(-) create mode 100644 MetaforceInstaller.UI/NewInstallationDialog.axaml create mode 100644 MetaforceInstaller.UI/NewInstallationDialog.axaml.cs create mode 100644 MetaforceInstaller.UI/TextDefaults.cs diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml index 61a426d..7b8c85a 100644 --- a/MetaforceInstaller.UI/MainWindow.axaml +++ b/MetaforceInstaller.UI/MainWindow.axaml @@ -10,11 +10,11 @@ - + - + - + @@ -22,49 +22,44 @@ + - - - - - - - + + - - - - + Install server + Install admin + Save android admin + Save VR client + + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs new file mode 100644 index 0000000..85d5f28 --- /dev/null +++ b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs @@ -0,0 +1,118 @@ +using System.IO.Compression; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using MetaforceInstaller.Core.Models; +using MetaforceInstaller.Core.Services; + +namespace MetaforceInstaller.UI; + +public partial class NewInstallationDialog : Window +{ + private string? _zipPath; + private InstallationParts? _installationParts; + + public NewInstallationDialog() + { + InitializeComponent(); + RefreshCheckboxes(); + CancelButton.Click += OnCancelClick; + ChooseZip.Click += OnChooseZipClick; + InstallButton.IsEnabled = false; + InstallButton.Click += OnInstallClick; + } + + private async void RefreshCheckboxes() + { + var serverCheckbox = ServerCheckBox; + var pcAdminCheckbox = PcAdminCheckBox; + var androidAdminCheckbox = AndroidAdminCheckbox; + var vrClientCheckbox = VrClientCheckbox; + serverCheckbox.Content = TextDefaults.InstallServer; + serverCheckbox.IsEnabled = true; + pcAdminCheckbox.Content = TextDefaults.InstallAdmin; + pcAdminCheckbox.IsEnabled = true; + androidAdminCheckbox.Content = TextDefaults.SaveAndroidAdmin; + androidAdminCheckbox.IsEnabled = true; + vrClientCheckbox.Content = TextDefaults.SaveVRClient; + vrClientCheckbox.IsEnabled = true; + } + + private async void OnChooseZipClick(object? sender, RoutedEventArgs e) + { + var topLevel = GetTopLevel(this); + var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Выберите архив с контентом", + AllowMultiple = false, + FileTypeFilter = + [ + new FilePickerFileType("ZIP Files") + { + Patterns = ["*.zip"] + } + ] + }); + + if (files.Count >= 1) + { + _zipPath = files[0].Path.LocalPath; + using var archive = ZipFile.OpenRead(_zipPath); + _installationParts = ZipScrapper.PeekFiles(archive); + UpdateCheckboxes(); + archive.Dispose(); + } + } + + private async void OnInstallClick(object? sender, RoutedEventArgs e) + { + using var archive = ZipFile.OpenRead(_zipPath); + + } + + private void UpdateCheckboxes() + { + RefreshCheckboxes(); + var serverCheckbox = ServerCheckBox; + var pcAdminCheckbox = PcAdminCheckBox; + var androidAdminCheckbox = AndroidAdminCheckbox; + var vrClientCheckbox = VrClientCheckbox; + + if (!_installationParts.WindowsServerExists) + { + serverCheckbox.IsEnabled = false; + serverCheckbox.Content += "\nCouldn't find directory with server"; + } + + if (!_installationParts.WindowsAdminExists) + { + pcAdminCheckbox.IsEnabled = false; + pcAdminCheckbox.Content += "\nCouldn't find directory with PC admin"; + } + + if (!_installationParts.WindowsContentExists) + { + pcAdminCheckbox.IsEnabled = false; + pcAdminCheckbox.Content += "\nCouldn't find windows content"; + } + + if (!_installationParts.AndroidContentExists) + { + vrClientCheckbox.IsEnabled = false; + vrClientCheckbox.Content += "\nCouldn't find android content"; + androidAdminCheckbox.IsEnabled = false; + androidAdminCheckbox.Content += "\nCouldn't find android content"; + } + + if (!_installationParts.PicoClientExists && !_installationParts.OculusClientExists) + { + vrClientCheckbox.IsEnabled = false; + vrClientCheckbox.Content += "\nCouldn't find any VR clients"; + } + } + + private void OnCancelClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.UI/TextDefaults.cs b/MetaforceInstaller.UI/TextDefaults.cs new file mode 100644 index 0000000..176e1f6 --- /dev/null +++ b/MetaforceInstaller.UI/TextDefaults.cs @@ -0,0 +1,9 @@ +namespace MetaforceInstaller.UI; + +public static class TextDefaults +{ + public static readonly string InstallServer = "Install Server"; + public static readonly string InstallAdmin = "Install Admin"; + public static readonly string SaveAndroidAdmin = "Save Android admin"; + public static readonly string SaveVRClient = "Save VR client"; +} \ No newline at end of file From c6a0b39ded84f34b382815c2c1b35dfbc89e159b Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 05:46:42 +0500 Subject: [PATCH 04/11] scrap paths directly --- .../Models/InstallationParts.cs | 14 ++-- .../Services/ZipScrapper.cs | 83 ++++++++++--------- .../NewInstallationDialog.axaml.cs | 14 ++-- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/MetaforceInstaller.Core/Models/InstallationParts.cs b/MetaforceInstaller.Core/Models/InstallationParts.cs index cb531ab..e4c1555 100644 --- a/MetaforceInstaller.Core/Models/InstallationParts.cs +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -2,11 +2,11 @@ public record InstallationParts { - public bool OculusClientExists { get; init; } - public bool PicoClientExists { get; init; } - public bool AndroidAdminExists { get; init; } - public bool AndroidContentExists { get; init; } - public bool WindowsContentExists { get; init; } - public bool WindowsAdminExists { get; init; } - public bool WindowsServerExists { get; init; } + public string? OculusClientPath { get; init; } + public string? PicoClientPath { get; init; } + public string? AndroidAdminPath { get; init; } + public string? AndroidContentPath { get; init; } + public string? WindowsContentPath { get; init; } + public string? WindowsAdminPath { get; init; } + public string? WindowsServerPath { get; init; } } \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs index b5d1a32..8582a68 100644 --- a/MetaforceInstaller.Core/Services/ZipScrapper.cs +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -9,13 +9,13 @@ public class ZipScrapper { return new InstallationParts { - AndroidContentExists = DoesAndroidContentExists(archive), - OculusClientExists = DoesOculusClientExists(archive), - PicoClientExists = DoesPicoClientExists(archive), - AndroidAdminExists = DoesAndroidAdminExists(archive), - WindowsAdminExists = DoesPcAdminExists(archive), - WindowsContentExists = DoesWindowsContentExists(archive), - WindowsServerExists = DoesServerExists(archive), + AndroidContentPath = DoesAndroidContentExists(archive), + OculusClientPath = DoesOculusClientExists(archive), + PicoClientPath = DoesPicoClientExists(archive), + AndroidAdminPath = DoesAndroidAdminExists(archive), + WindowsAdminPath = DoesPcAdminExists(archive), + WindowsContentPath = DoesWindowsContentExists(archive), + WindowsServerPath = DoesServerExists(archive), }; // Console.WriteLine($"Contents of {archive}:"); // Console.WriteLine("----------------------------------"); @@ -36,54 +36,63 @@ public class ZipScrapper return outputPath; } - private static bool DoesPicoClientExists(ZipArchive archive) + private static string? DoesPicoClientExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.Name.Contains("MetaforcePico") - && entry.Name.EndsWith(".apk")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.Name.Contains("MetaforcePico") + && entry.Name.EndsWith(".apk")); + return entry?.FullName; } - private static bool DoesOculusClientExists(ZipArchive archive) + private static string? DoesOculusClientExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.Name.Contains("MetaforceOculus") - && entry.Name.EndsWith(".apk")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.Name.Contains("MetaforceOculus") + && entry.Name.EndsWith(".apk")); + return entry?.FullName; } - private static bool DoesAndroidAdminExists(ZipArchive archive) + private static string? DoesAndroidAdminExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.Name.Contains("MetaforceAdmin") - && entry.Name.EndsWith(".apk")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.Name.Contains("MetaforceAdmin") + && entry.Name.EndsWith(".apk")); + return entry?.FullName; } - private static bool DoesAndroidContentExists(ZipArchive archive) + private static string? DoesAndroidContentExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.Name.Contains("Content_Android") - && entry.Name.EndsWith(".zip")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.Name.Contains("Content_Android") + && entry.Name.EndsWith(".zip")); + return entry?.FullName; } - private static bool DoesWindowsContentExists(ZipArchive archive) + private static string? DoesWindowsContentExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.Name.Contains("Content_StandaloneWindows") - && entry.Name.EndsWith(".zip")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.Name.Contains("Content_StandaloneWindows") + && entry.Name.EndsWith(".zip")); + return entry?.FullName; } - private static bool DoesPcAdminExists(ZipArchive archive) + private static string? DoesPcAdminExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.FullName.Contains("MetaforceAdminPC") - && entry.Name.EndsWith(".exe") && !entry.Name.Contains("UnityCrashHandler") && - !entry.Name.Contains("crashpad_handler")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.FullName.Contains("MetaforceAdminPC") && + entry.Name.EndsWith(".exe") + && !entry.Name.Contains("UnityCrashHandler") && + !entry.Name.Contains("crashpad_handler")); + return entry?.FullName; } - private static bool DoesServerExists(ZipArchive archive) + private static string? DoesServerExists(ZipArchive archive) { - return archive.Entries - .Any(entry => entry.FullName.Contains("MetaforceServer") - && entry.Name.EndsWith(".exe") && !entry.Name.Contains("UnityCrashHandler") && - !entry.Name.Contains("crashpad_handler")); + var entry = archive.Entries.FirstOrDefault(entry => + entry.FullName.Contains("MetaforceServer") && + entry.Name.EndsWith(".exe") + && !entry.Name.Contains("UnityCrashHandler") && + !entry.Name.Contains("crashpad_handler")); + return entry?.FullName; } } \ No newline at end of file diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs index 85d5f28..e144dd3 100644 --- a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs +++ b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs @@ -22,7 +22,7 @@ public partial class NewInstallationDialog : Window InstallButton.Click += OnInstallClick; } - private async void RefreshCheckboxes() + private void RefreshCheckboxes() { var serverCheckbox = ServerCheckBox; var pcAdminCheckbox = PcAdminCheckBox; @@ -67,7 +67,6 @@ public partial class NewInstallationDialog : Window private async void OnInstallClick(object? sender, RoutedEventArgs e) { using var archive = ZipFile.OpenRead(_zipPath); - } private void UpdateCheckboxes() @@ -78,25 +77,25 @@ public partial class NewInstallationDialog : Window var androidAdminCheckbox = AndroidAdminCheckbox; var vrClientCheckbox = VrClientCheckbox; - if (!_installationParts.WindowsServerExists) + if (string.IsNullOrEmpty(_installationParts.WindowsServerPath)) { serverCheckbox.IsEnabled = false; serverCheckbox.Content += "\nCouldn't find directory with server"; } - if (!_installationParts.WindowsAdminExists) + if (string.IsNullOrEmpty(_installationParts.WindowsAdminPath)) { pcAdminCheckbox.IsEnabled = false; pcAdminCheckbox.Content += "\nCouldn't find directory with PC admin"; } - if (!_installationParts.WindowsContentExists) + if (string.IsNullOrEmpty(_installationParts.WindowsContentPath)) { pcAdminCheckbox.IsEnabled = false; pcAdminCheckbox.Content += "\nCouldn't find windows content"; } - if (!_installationParts.AndroidContentExists) + if (string.IsNullOrEmpty(_installationParts.AndroidContentPath)) { vrClientCheckbox.IsEnabled = false; vrClientCheckbox.Content += "\nCouldn't find android content"; @@ -104,7 +103,8 @@ public partial class NewInstallationDialog : Window androidAdminCheckbox.Content += "\nCouldn't find android content"; } - if (!_installationParts.PicoClientExists && !_installationParts.OculusClientExists) + if (string.IsNullOrEmpty(_installationParts.PicoClientPath) && + string.IsNullOrEmpty(_installationParts.OculusClientPath)) { vrClientCheckbox.IsEnabled = false; vrClientCheckbox.Content += "\nCouldn't find any VR clients"; From 297a784956e97859ac5ebee2a3af9122a5c3f17e Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 20:13:47 +0500 Subject: [PATCH 05/11] make zip unpacking and saving data to storage --- MetaforceInstaller.Core/Defaults.cs | 7 +++++ .../Models/InstallationData.cs | 6 ++--- .../Models/InstallationParts.cs | 16 ++++++++++- .../Services/StorageService.cs | 3 +-- .../Services/ZipScrapper.cs | 24 +++++++++++++++-- .../NewInstallationDialog.axaml | 7 +++-- .../NewInstallationDialog.axaml.cs | 27 ++++++++++++++++++- 7 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 MetaforceInstaller.Core/Defaults.cs diff --git a/MetaforceInstaller.Core/Defaults.cs b/MetaforceInstaller.Core/Defaults.cs new file mode 100644 index 0000000..ec22aad --- /dev/null +++ b/MetaforceInstaller.Core/Defaults.cs @@ -0,0 +1,7 @@ +namespace MetaforceInstaller.Core; + +public static class Defaults +{ + public static readonly string StoragePath = + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + "MetaforceInstaller"; +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationData.cs b/MetaforceInstaller.Core/Models/InstallationData.cs index 855fae7..9eadeec 100644 --- a/MetaforceInstaller.Core/Models/InstallationData.cs +++ b/MetaforceInstaller.Core/Models/InstallationData.cs @@ -3,8 +3,6 @@ public class InstallationData { public Guid Id { get; set; } = Guid.NewGuid(); - public string Title { get; set; } - public string AndroidPackagePath { get; set; } - public string WindowsServerPackagePath { get; set; } - public string WindowsAdminPackagePath { get; set; } + public required string Title { get; set; } + public required InstallationParts Parts { get; set; } } \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationParts.cs b/MetaforceInstaller.Core/Models/InstallationParts.cs index e4c1555..807c902 100644 --- a/MetaforceInstaller.Core/Models/InstallationParts.cs +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -1,6 +1,6 @@ namespace MetaforceInstaller.Core.Models; -public record InstallationParts +public class InstallationParts { public string? OculusClientPath { get; init; } public string? PicoClientPath { get; init; } @@ -9,4 +9,18 @@ public record InstallationParts public string? WindowsContentPath { get; init; } public string? WindowsAdminPath { get; init; } public string? WindowsServerPath { get; init; } + + public InstallationParts ExtendPaths(string prefixPath) + { + return new InstallationParts + { + OculusClientPath = OculusClientPath is null ? null : Path.Combine(prefixPath, OculusClientPath), + PicoClientPath = PicoClientPath is null ? null : Path.Combine(prefixPath, PicoClientPath), + AndroidAdminPath = AndroidAdminPath is null ? null : Path.Combine(prefixPath, AndroidAdminPath), + AndroidContentPath = AndroidContentPath is null ? null : Path.Combine(prefixPath, AndroidContentPath), + WindowsContentPath = WindowsContentPath is null ? null : Path.Combine(prefixPath, WindowsContentPath), + WindowsAdminPath = WindowsAdminPath is null ? null : Path.Combine(prefixPath, WindowsAdminPath), + WindowsServerPath = WindowsServerPath is null ? null : Path.Combine(prefixPath, WindowsServerPath) + }; + } } \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/StorageService.cs b/MetaforceInstaller.Core/Services/StorageService.cs index 7fa1601..4ab4ffa 100644 --- a/MetaforceInstaller.Core/Services/StorageService.cs +++ b/MetaforceInstaller.Core/Services/StorageService.cs @@ -10,8 +10,7 @@ public class StorageService : IStorageService public StorageService() { - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var appDirectory = Path.Combine(appData, "MetaforceInstaller"); + var appDirectory = Defaults.StoragePath; Directory.CreateDirectory(appDirectory); _storagePath = Path.Combine(appDirectory, "installations.json"); } diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs index 8582a68..7a4496a 100644 --- a/MetaforceInstaller.Core/Services/ZipScrapper.cs +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -30,9 +30,29 @@ public class ZipScrapper // } } - public static string ExtractZip(ZipArchive archive, string outputPath) + public static string ExtractZip(ZipArchive archive, string outputPath, IProgress? progress = null) { - archive.ExtractToDirectory(outputPath); + var entries = archive.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToList(); + var totalEntries = entries.Count; + var processedEntries = 0; + + foreach (var entry in entries) + { + var destinationPath = Path.Combine(outputPath, entry.FullName); + var destinationDir = Path.GetDirectoryName(destinationPath); + + if (!string.IsNullOrEmpty(destinationDir)) + { + Directory.CreateDirectory(destinationDir); + Console.WriteLine($"Extracting {entry.FullName} to {destinationPath}"); + } + + entry.ExtractToFile(destinationPath, overwrite: true); + + processedEntries++; + progress?.Report((double)processedEntries / totalEntries * 100); + } + return outputPath; } diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml b/MetaforceInstaller.UI/NewInstallationDialog.axaml index 29a5f78..1208fdd 100644 --- a/MetaforceInstaller.UI/NewInstallationDialog.axaml +++ b/MetaforceInstaller.UI/NewInstallationDialog.axaml @@ -4,11 +4,13 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="400" x:Class="MetaforceInstaller.UI.NewInstallationDialog" - Title="MetaforceInstaller"> + Title="MetaforceInstaller - Add new installation" + SizeToContent="WidthAndHeight" + CanResize="False"> - + Install server Install admin @@ -19,6 +21,7 @@ + \ No newline at end of file diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs index e144dd3..e592b8c 100644 --- a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs +++ b/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs @@ -1,7 +1,13 @@ -using System.IO.Compression; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; +using MetaforceInstaller.Core; using MetaforceInstaller.Core.Models; using MetaforceInstaller.Core.Services; @@ -67,6 +73,21 @@ public partial class NewInstallationDialog : Window private async void OnInstallClick(object? sender, RoutedEventArgs e) { using var archive = ZipFile.OpenRead(_zipPath); + var title = TitleTextBox.Text ?? Path.GetFileNameWithoutExtension(_zipPath); + var progress = new Progress(value => { ProgressBar.Value = value; }); + await Task.Run(() => + ZipScrapper.ExtractZip(archive, Defaults.StoragePath, progress)); + InstallButton.IsEnabled = false; + var storageService = new StorageService(); + var appData = storageService.Load(); + var installationData = new InstallationData + { + Title = title, + Parts = _installationParts.ExtendPaths(Defaults.StoragePath + Path.DirectorySeparatorChar) + }; + appData.Installations.Add(installationData); + storageService.Save(appData); + Close(); } private void UpdateCheckboxes() @@ -77,6 +98,7 @@ public partial class NewInstallationDialog : Window var androidAdminCheckbox = AndroidAdminCheckbox; var vrClientCheckbox = VrClientCheckbox; + if (string.IsNullOrEmpty(_installationParts.WindowsServerPath)) { serverCheckbox.IsEnabled = false; @@ -109,6 +131,9 @@ public partial class NewInstallationDialog : Window vrClientCheckbox.IsEnabled = false; vrClientCheckbox.Content += "\nCouldn't find any VR clients"; } + + InstallButton.IsEnabled = new List + { serverCheckbox, pcAdminCheckbox, androidAdminCheckbox, vrClientCheckbox }.Any(x => x?.IsEnabled == true); } private void OnCancelClick(object? sender, RoutedEventArgs e) From 89c3dcb424caa171d4a30222a9c6d9bf61098d16 Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 20:32:38 +0500 Subject: [PATCH 06/11] update mainwindow so it shows real installations --- MetaforceInstaller.UI/MainWindow.axaml | 39 ++- MetaforceInstaller.UI/MainWindow.axaml.cs | 234 ++---------------- .../ViewModels/MainWindowViewModel.cs | 28 +++ 3 files changed, 65 insertions(+), 236 deletions(-) create mode 100644 MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml index 7b8c85a..8b8276a 100644 --- a/MetaforceInstaller.UI/MainWindow.axaml +++ b/MetaforceInstaller.UI/MainWindow.axaml @@ -2,7 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:MetaforceInstaller.UI.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:CompileBindings="True" + x:DataType="vm:MainWindowViewModel" x:Class="MetaforceInstaller.UI.MainWindow" Title="MetaforceInstaller"> @@ -19,6 +22,10 @@ + + + + @@ -39,27 +46,17 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/MainWindow.axaml.cs b/MetaforceInstaller.UI/MainWindow.axaml.cs index 975928c..9e5ac6b 100644 --- a/MetaforceInstaller.UI/MainWindow.axaml.cs +++ b/MetaforceInstaller.UI/MainWindow.axaml.cs @@ -1,236 +1,40 @@ -using System; -using System.IO; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Platform.Storage; -using Avalonia.Threading; -using Avalonia.VisualTree; -using MetaforceInstaller.Core.Models; +using MetaforceInstaller.Core.Intefaces; using MetaforceInstaller.Core.Services; +using MetaforceInstaller.UI.ViewModels; namespace MetaforceInstaller.UI; public partial class MainWindow : Window { - private string? _apkPath; - private string? _zipPath; - private AdbService _adbService; - - private const int PROGRESS_LOG_STEP = 10; - private const int PROGRESS_UPDATE_STEP = 1; - - private int _lastLoggedProgress = -1; - private int _lastUpdatedProgress = -1; + private MainWindowViewModel _viewModel; + private IStorageService _storageService; public MainWindow() { InitializeComponent(); + + _viewModel = new MainWindowViewModel(); + DataContext = _viewModel; + + _storageService = new StorageService(); + NewInstallationButton.Click += OnNewInstalltionClick; - // LogMessage("MetaforceInstaller by slavagm"); - // - // _adbService = new AdbService(); - // _adbService.ProgressChanged += OnAdbProgressChanged; - // _adbService.StatusChanged += OnAdbStatusChanged; - // - // CheckAndEnableInstallButton(); - // - // ChooseApkButton.Click += OnChooseApkClicked; - // ChooseContentButton.Click += OnChooseContentClicked; - // InstallButton.Click += OnInstallClicked; + + LoadInstallations(); + } + + private void LoadInstallations() + { + var appData = _storageService.Load(); + _viewModel.LoadInstallations(appData.Installations); } public async void OnNewInstalltionClick(object? sender, RoutedEventArgs e) { var newInstallationDialog = new NewInstallationDialog(); await newInstallationDialog.ShowDialog(this); + LoadInstallations(); } - - // private void OnAdbProgressChanged(object? sender, ProgressInfo e) - // { - // Dispatcher.UIThread.InvokeAsync(() => - // { - // if (e.PercentageComplete != _lastUpdatedProgress && - // e.PercentageComplete % PROGRESS_UPDATE_STEP == 0) - // { - // InstallProgressBar.Value = e.PercentageComplete; - // _lastUpdatedProgress = e.PercentageComplete; - // } - // - // if (e.PercentageComplete != _lastLoggedProgress && - // e.PercentageComplete % PROGRESS_LOG_STEP == 0 || e.PercentageComplete == 100) - // { - // LogMessage( - // e.TotalBytes > 0 - // ? $"Прогресс: {e.PercentageComplete}% ({FormatBytes(e.BytesTransferred)} / {FormatBytes(e.TotalBytes)})" - // : $"Прогресс: {e.PercentageComplete}%"); - // - // _lastLoggedProgress = e.PercentageComplete; - // } - // }); - // } - // - // private void OnProgressReport(ProgressInfo progressInfo) - // { - // Dispatcher.UIThread.InvokeAsync(() => - // { - // if (progressInfo.PercentageComplete != _lastUpdatedProgress && - // progressInfo.PercentageComplete % PROGRESS_UPDATE_STEP == 0) - // { - // InstallProgressBar.Value = progressInfo.PercentageComplete; - // _lastUpdatedProgress = progressInfo.PercentageComplete; - // } - // - // - // if (progressInfo.PercentageComplete != _lastLoggedProgress && - // (progressInfo.PercentageComplete % PROGRESS_LOG_STEP == 0 || progressInfo.PercentageComplete == 100)) - // { - // LogMessage( - // progressInfo.TotalBytes > 0 - // ? $"Прогресс: {progressInfo.PercentageComplete}% ({FormatBytes(progressInfo.BytesTransferred)} / {FormatBytes(progressInfo.TotalBytes)})" - // : $"Прогресс: {progressInfo.PercentageComplete}%"); - // - // _lastLoggedProgress = progressInfo.PercentageComplete; - // } - // }); - // } - // - // private void OnAdbStatusChanged(object? sender, string e) - // { - // Dispatcher.UIThread.InvokeAsync(() => - // { - // InstallProgressBar.Value = 0; - // _lastLoggedProgress = -1; - // _lastUpdatedProgress = -1; - // LogMessage(e); - // }); - // } - // - // private string FormatBytes(long bytes) - // { - // string[] suffixes = ["B", "KB", "MB", "GB", "TB"]; - // var counter = 0; - // double number = bytes; - // - // while (Math.Round(number / 1024) >= 1) - // { - // number /= 1024; - // counter++; - // } - // - // return $"{number:N1} {suffixes[counter]}"; - // } - // - // - // private async void CheckAndEnableInstallButton() - // { - // InstallButton.IsEnabled = !string.IsNullOrEmpty(_apkPath) && !string.IsNullOrEmpty(_zipPath); - // } - // - // private async void OnChooseApkClicked(object? sender, RoutedEventArgs e) - // { - // var topLevel = GetTopLevel(this); - // var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - // { - // Title = "Выберите APK файл", - // AllowMultiple = false, - // FileTypeFilter = new[] - // { - // new FilePickerFileType("APK Files") - // { - // Patterns = new[] { "*.apk" } - // } - // } - // }); - // - // if (files.Count >= 1) - // { - // _apkPath = files[0].Path.LocalPath; - // LogMessage($"APK выбран: {Path.GetFileName(_apkPath)}"); - // } - // - // CheckAndEnableInstallButton(); - // } - // - // private async void OnChooseContentClicked(object? sender, RoutedEventArgs e) - // { - // var topLevel = GetTopLevel(this); - // var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - // { - // Title = "Выберите архив с контентом", - // AllowMultiple = false, - // FileTypeFilter = new[] - // { - // new FilePickerFileType("ZIP Files") - // { - // Patterns = new[] { "*.zip" } - // } - // } - // }); - // - // if (files.Count >= 1) - // { - // _zipPath = files[0].Path.LocalPath; - // LogMessage($"Контент выбран: {Path.GetFileName(_zipPath)}"); - // } - // - // CheckAndEnableInstallButton(); - // } - // - // private async void OnInstallClicked(object? sender, RoutedEventArgs e) - // { - // if (string.IsNullOrEmpty(_apkPath) || string.IsNullOrEmpty(_zipPath)) - // { - // LogMessage("Ошибка: Выберите APK файл и папку с контентом"); - // return; - // } - // - // _adbService.RefreshDeviceData(); - // - // InstallButton.IsEnabled = false; - // InstallProgressBar.Value = 0; - // - // try - // { - // LogMessage("Начинаем установку..."); - // - // var deviceInfo = _adbService.GetDeviceInfo(); - // LogMessage($"Найдено устройство: {deviceInfo.SerialNumber}"); - // LogMessage($"Состояние: {deviceInfo.State}"); - // LogMessage($"Модель: {deviceInfo.Model} - {deviceInfo.Name}"); - // - // var progress = new Progress(OnProgressReport); - // - // await _adbService.InstallApkAsync(_apkPath, progress); - // - // var apkInfo = ApkScrapper.GetApkInfo(_apkPath); - // LogMessage($"Ставим {apkInfo.PackageName} версии {apkInfo.VersionName}"); - // var zipName = Path.GetFileName(_zipPath); - // var outputPath = - // @$"/storage/emulated/0/Android/data/{apkInfo.PackageName}/files/{zipName}"; - // LogMessage($"Начинаем копирование контента в {outputPath}"); - // - // await _adbService.CopyFileAsync(_zipPath, outputPath, progress); - // - // LogMessage("Установка завершена успешно!"); - // } - // catch (Exception ex) - // { - // LogMessage($"Ошибка установки: {ex.Message}"); - // } - // finally - // { - // InstallButton.IsEnabled = true; - // InstallProgressBar.Value = 0; - // } - // } - // - // private void LogMessage(string message) - // { - // var timestamp = DateTime.Now.ToString("HH:mm:ss"); - // LogsTextBox.Text += $"[{timestamp}] {message}\n"; - // - // var scrollViewer = LogsTextBox.FindAncestorOfType(); - // scrollViewer?.ScrollToEnd(); - // } } \ No newline at end of file diff --git a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..6a81dcd --- /dev/null +++ b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.UI.ViewModels; + +public class MainWindowViewModel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + public ObservableCollection Installations { get; set; } = new(); + + public void LoadInstallations(IEnumerable data) + { + Installations.Clear(); + foreach (var installation in data) + { + Installations.Add(installation); + } + } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file From bbf905495c608f3f1bd798b4b4168c807ed9a609 Mon Sep 17 00:00:00 2001 From: shept Date: Thu, 1 Jan 2026 22:58:33 +0500 Subject: [PATCH 07/11] some refactoring --- .../Models/InstallationData.cs | 1 + .../Models/InstallationParts.cs | 14 -- .../Services/ZipScrapper.cs | 190 +++++++++++------- MetaforceInstaller.UI/App.axaml.cs | 1 + .../{ => Windows}/MainWindow.axaml | 16 +- .../{ => Windows}/MainWindow.axaml.cs | 7 +- .../{ => Windows}/NewInstallationDialog.axaml | 2 +- .../NewInstallationDialog.axaml.cs | 35 +++- 8 files changed, 160 insertions(+), 106 deletions(-) rename MetaforceInstaller.UI/{ => Windows}/MainWindow.axaml (75%) rename MetaforceInstaller.UI/{ => Windows}/MainWindow.axaml.cs (88%) rename MetaforceInstaller.UI/{ => Windows}/NewInstallationDialog.axaml (94%) rename MetaforceInstaller.UI/{ => Windows}/NewInstallationDialog.axaml.cs (85%) diff --git a/MetaforceInstaller.Core/Models/InstallationData.cs b/MetaforceInstaller.Core/Models/InstallationData.cs index 9eadeec..6f53baa 100644 --- a/MetaforceInstaller.Core/Models/InstallationData.cs +++ b/MetaforceInstaller.Core/Models/InstallationData.cs @@ -5,4 +5,5 @@ public class InstallationData public Guid Id { get; set; } = Guid.NewGuid(); public required string Title { get; set; } public required InstallationParts Parts { get; set; } + public DateTime InstalledAt { get; set; } = DateTime.Now; } \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationParts.cs b/MetaforceInstaller.Core/Models/InstallationParts.cs index 807c902..62acb62 100644 --- a/MetaforceInstaller.Core/Models/InstallationParts.cs +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -9,18 +9,4 @@ public class InstallationParts public string? WindowsContentPath { get; init; } public string? WindowsAdminPath { get; init; } public string? WindowsServerPath { get; init; } - - public InstallationParts ExtendPaths(string prefixPath) - { - return new InstallationParts - { - OculusClientPath = OculusClientPath is null ? null : Path.Combine(prefixPath, OculusClientPath), - PicoClientPath = PicoClientPath is null ? null : Path.Combine(prefixPath, PicoClientPath), - AndroidAdminPath = AndroidAdminPath is null ? null : Path.Combine(prefixPath, AndroidAdminPath), - AndroidContentPath = AndroidContentPath is null ? null : Path.Combine(prefixPath, AndroidContentPath), - WindowsContentPath = WindowsContentPath is null ? null : Path.Combine(prefixPath, WindowsContentPath), - WindowsAdminPath = WindowsAdminPath is null ? null : Path.Combine(prefixPath, WindowsAdminPath), - WindowsServerPath = WindowsServerPath is null ? null : Path.Combine(prefixPath, WindowsServerPath) - }; - } } \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs index 7a4496a..b5c4122 100644 --- a/MetaforceInstaller.Core/Services/ZipScrapper.cs +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -9,42 +9,46 @@ public class ZipScrapper { return new InstallationParts { - AndroidContentPath = DoesAndroidContentExists(archive), - OculusClientPath = DoesOculusClientExists(archive), - PicoClientPath = DoesPicoClientExists(archive), - AndroidAdminPath = DoesAndroidAdminExists(archive), - WindowsAdminPath = DoesPcAdminExists(archive), - WindowsContentPath = DoesWindowsContentExists(archive), - WindowsServerPath = DoesServerExists(archive), + AndroidContentPath = FindAndroidContent(archive), + OculusClientPath = FindOculusClient(archive), + PicoClientPath = FindPicoClient(archive), + AndroidAdminPath = FindAndroidAdmin(archive), + WindowsAdminPath = FindPcAdmin(archive), + WindowsContentPath = FindWindowsContent(archive), + WindowsServerPath = FindServer(archive), }; - // Console.WriteLine($"Contents of {archive}:"); - // Console.WriteLine("----------------------------------"); - // - // foreach (ZipArchiveEntry entry in archive.Entries) - // { - // // You can access properties like entry.Name, entry.FullName, entry.Length (uncompressed size), entry.CompressedLength - // Console.WriteLine($"Entry: {entry.FullName}"); - // Console.WriteLine($" Uncompressed Size: {entry.Length} bytes"); - // Console.WriteLine($" Compressed Size: {entry.CompressedLength} bytes"); - // Console.WriteLine("----------------------------------"); - // } } - public static string ExtractZip(ZipArchive archive, string outputPath, IProgress? progress = null) + /// + /// Extracts ZIP archive to a unique folder based on installation GUID + /// + /// ZIP archive to extract + /// Base storage path + /// Unique GUID for this installation + /// Progress reporter + /// Full path to the extracted folder + public static string ExtractZip( + ZipArchive archive, + string baseOutputPath, + Guid installationGuid, + IProgress? progress = null) { + // Create unique folder for this installation + var installationFolder = Path.Combine(baseOutputPath, installationGuid.ToString()); + Directory.CreateDirectory(installationFolder); + var entries = archive.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToList(); var totalEntries = entries.Count; var processedEntries = 0; foreach (var entry in entries) { - var destinationPath = Path.Combine(outputPath, entry.FullName); + var destinationPath = Path.Combine(installationFolder, entry.FullName); var destinationDir = Path.GetDirectoryName(destinationPath); if (!string.IsNullOrEmpty(destinationDir)) { Directory.CreateDirectory(destinationDir); - Console.WriteLine($"Extracting {entry.FullName} to {destinationPath}"); } entry.ExtractToFile(destinationPath, overwrite: true); @@ -53,66 +57,104 @@ public class ZipScrapper progress?.Report((double)processedEntries / totalEntries * 100); } - return outputPath; + return installationFolder; } - private static string? DoesPicoClientExists(ZipArchive archive) + /// + /// Updates InstallationParts paths to reflect the actual extracted location + /// + public static InstallationParts UpdatePathsAfterExtraction( + InstallationParts parts, + string extractedFolderPath) { - var entry = archive.Entries.FirstOrDefault(entry => - entry.Name.Contains("MetaforcePico") - && entry.Name.EndsWith(".apk")); + return new InstallationParts + { + AndroidContentPath = UpdatePath(parts.AndroidContentPath, extractedFolderPath), + OculusClientPath = UpdatePath(parts.OculusClientPath, extractedFolderPath), + PicoClientPath = UpdatePath(parts.PicoClientPath, extractedFolderPath), + AndroidAdminPath = UpdatePath(parts.AndroidAdminPath, extractedFolderPath), + WindowsAdminPath = UpdatePath(parts.WindowsAdminPath, extractedFolderPath), + WindowsContentPath = UpdatePath(parts.WindowsContentPath, extractedFolderPath), + WindowsServerPath = UpdatePath(parts.WindowsServerPath, extractedFolderPath), + }; + } + + private static string? UpdatePath(string? relativePath, string basePath) + { + if (string.IsNullOrEmpty(relativePath)) + return null; + + return Path.Combine(basePath, relativePath); + } + + private static string? FindPicoClient(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforcePico", + extension: ".apk"); + } + + private static string? FindOculusClient(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforceOculus", + extension: ".apk"); + } + + private static string? FindAndroidAdmin(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforceAdmin", + extension: ".apk"); + } + + private static string? FindAndroidContent(ZipArchive archive) + { + return FindEntry(archive, + name: "Content_Android", + extension: ".zip"); + } + + private static string? FindWindowsContent(ZipArchive archive) + { + return FindEntry(archive, + name: "Content_StandaloneWindows", + extension: ".zip"); + } + + private static string? FindPcAdmin(ZipArchive archive) + { + return FindExecutable(archive, "MetaforceAdminPC"); + } + + private static string? FindServer(ZipArchive archive) + { + return FindExecutable(archive, "MetaforceServer"); + } + + /// + /// Finds an entry in archive by name and extension + /// + private static string? FindEntry(ZipArchive archive, string name, string extension) + { + var entry = archive.Entries.FirstOrDefault(e => + e.Name.Contains(name, StringComparison.OrdinalIgnoreCase) && + e.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + return entry?.FullName; } - private static string? DoesOculusClientExists(ZipArchive archive) + /// + /// Finds an executable in archive, excluding crash handlers + /// + private static string? FindExecutable(ZipArchive archive, string containsName) { - var entry = archive.Entries.FirstOrDefault(entry => - entry.Name.Contains("MetaforceOculus") - && entry.Name.EndsWith(".apk")); - return entry?.FullName; - } - - private static string? DoesAndroidAdminExists(ZipArchive archive) - { - var entry = archive.Entries.FirstOrDefault(entry => - entry.Name.Contains("MetaforceAdmin") - && entry.Name.EndsWith(".apk")); - return entry?.FullName; - } - - private static string? DoesAndroidContentExists(ZipArchive archive) - { - var entry = archive.Entries.FirstOrDefault(entry => - entry.Name.Contains("Content_Android") - && entry.Name.EndsWith(".zip")); - return entry?.FullName; - } - - private static string? DoesWindowsContentExists(ZipArchive archive) - { - var entry = archive.Entries.FirstOrDefault(entry => - entry.Name.Contains("Content_StandaloneWindows") - && entry.Name.EndsWith(".zip")); - return entry?.FullName; - } - - private static string? DoesPcAdminExists(ZipArchive archive) - { - var entry = archive.Entries.FirstOrDefault(entry => - entry.FullName.Contains("MetaforceAdminPC") && - entry.Name.EndsWith(".exe") - && !entry.Name.Contains("UnityCrashHandler") && - !entry.Name.Contains("crashpad_handler")); - return entry?.FullName; - } - - private static string? DoesServerExists(ZipArchive archive) - { - var entry = archive.Entries.FirstOrDefault(entry => - entry.FullName.Contains("MetaforceServer") && - entry.Name.EndsWith(".exe") - && !entry.Name.Contains("UnityCrashHandler") && - !entry.Name.Contains("crashpad_handler")); + var entry = archive.Entries.FirstOrDefault(e => + e.FullName.Contains(containsName, StringComparison.OrdinalIgnoreCase) && + e.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) && + !e.Name.Contains("UnityCrashHandler", StringComparison.OrdinalIgnoreCase) && + !e.Name.Contains("crashpad_handler", StringComparison.OrdinalIgnoreCase)); + return entry?.FullName; } } \ No newline at end of file diff --git a/MetaforceInstaller.UI/App.axaml.cs b/MetaforceInstaller.UI/App.axaml.cs index 16212ac..fc9da21 100644 --- a/MetaforceInstaller.UI/App.axaml.cs +++ b/MetaforceInstaller.UI/App.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using MetaforceInstaller.UI.Windows; namespace MetaforceInstaller.UI; diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml similarity index 75% rename from MetaforceInstaller.UI/MainWindow.axaml rename to MetaforceInstaller.UI/Windows/MainWindow.axaml index 8b8276a..fa144a4 100644 --- a/MetaforceInstaller.UI/MainWindow.axaml +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml @@ -6,7 +6,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:CompileBindings="True" x:DataType="vm:MainWindowViewModel" - x:Class="MetaforceInstaller.UI.MainWindow" + x:Class="MetaforceInstaller.UI.Windows.MainWindow" Title="MetaforceInstaller"> @@ -22,9 +22,9 @@ - + - + @@ -49,9 +49,13 @@ - - - + + + + + + + diff --git a/MetaforceInstaller.UI/MainWindow.axaml.cs b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs similarity index 88% rename from MetaforceInstaller.UI/MainWindow.axaml.cs rename to MetaforceInstaller.UI/Windows/MainWindow.axaml.cs index 9e5ac6b..d628bdd 100644 --- a/MetaforceInstaller.UI/MainWindow.axaml.cs +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs @@ -4,7 +4,7 @@ using MetaforceInstaller.Core.Intefaces; using MetaforceInstaller.Core.Services; using MetaforceInstaller.UI.ViewModels; -namespace MetaforceInstaller.UI; +namespace MetaforceInstaller.UI.Windows; public partial class MainWindow : Window { @@ -16,9 +16,8 @@ public partial class MainWindow : Window InitializeComponent(); _viewModel = new MainWindowViewModel(); - DataContext = _viewModel; - _storageService = new StorageService(); + DataContext = _viewModel; NewInstallationButton.Click += OnNewInstalltionClick; @@ -33,7 +32,7 @@ public partial class MainWindow : Window public async void OnNewInstalltionClick(object? sender, RoutedEventArgs e) { - var newInstallationDialog = new NewInstallationDialog(); + var newInstallationDialog = new NewInstallationDialog(_storageService); await newInstallationDialog.ShowDialog(this); LoadInstallations(); } diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml similarity index 94% rename from MetaforceInstaller.UI/NewInstallationDialog.axaml rename to MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml index 1208fdd..603e651 100644 --- a/MetaforceInstaller.UI/NewInstallationDialog.axaml +++ b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="400" - x:Class="MetaforceInstaller.UI.NewInstallationDialog" + x:Class="MetaforceInstaller.UI.Windows.NewInstallationDialog" Title="MetaforceInstaller - Add new installation" SizeToContent="WidthAndHeight" CanResize="False"> diff --git a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs similarity index 85% rename from MetaforceInstaller.UI/NewInstallationDialog.axaml.cs rename to MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs index e592b8c..f3f39a8 100644 --- a/MetaforceInstaller.UI/NewInstallationDialog.axaml.cs +++ b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs @@ -8,19 +8,24 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using MetaforceInstaller.Core; +using MetaforceInstaller.Core.Intefaces; using MetaforceInstaller.Core.Models; using MetaforceInstaller.Core.Services; -namespace MetaforceInstaller.UI; +namespace MetaforceInstaller.UI.Windows; public partial class NewInstallationDialog : Window { private string? _zipPath; private InstallationParts? _installationParts; + private readonly IStorageService _storageService; - public NewInstallationDialog() + public NewInstallationDialog(IStorageService storageService) { InitializeComponent(); + + _storageService = storageService; + RefreshCheckboxes(); CancelButton.Click += OnCancelClick; ChooseZip.Click += OnChooseZipClick; @@ -74,19 +79,35 @@ public partial class NewInstallationDialog : Window { using var archive = ZipFile.OpenRead(_zipPath); var title = TitleTextBox.Text ?? Path.GetFileNameWithoutExtension(_zipPath); + var installationGuid = Guid.NewGuid(); + var progress = new Progress(value => { ProgressBar.Value = value; }); + + string extractedPath = null; await Task.Run(() => - ZipScrapper.ExtractZip(archive, Defaults.StoragePath, progress)); + { + extractedPath = ZipScrapper.ExtractZip( + archive, + Defaults.StoragePath, + installationGuid, + progress); + }); + InstallButton.IsEnabled = false; - var storageService = new StorageService(); - var appData = storageService.Load(); + + var appData = _storageService.Load(); + + var updatedParts = ZipScrapper.UpdatePathsAfterExtraction(_installationParts, extractedPath); + var installationData = new InstallationData { + Id = installationGuid, Title = title, - Parts = _installationParts.ExtendPaths(Defaults.StoragePath + Path.DirectorySeparatorChar) + Parts = updatedParts }; + appData.Installations.Add(installationData); - storageService.Save(appData); + _storageService.Save(appData); Close(); } From 21fc25741b288cbc8d20485c0ecc40c3777ef95c Mon Sep 17 00:00:00 2001 From: shept Date: Fri, 2 Jan 2026 17:19:36 +0500 Subject: [PATCH 08/11] small ui updates --- .../MetaforceInstaller.UI.csproj | 2 +- .../Windows/MainWindow.axaml | 29 +++++++++--- .../Windows/MainWindow.axaml.cs | 44 +++++++++++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj index 26ffaf3..ad61469 100644 --- a/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj +++ b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj @@ -8,7 +8,7 @@ true app.manifest true - 1.2.1 + 2.0.0-b1 diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml index fa144a4..eaaf434 100644 --- a/MetaforceInstaller.UI/Windows/MainWindow.axaml +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml @@ -31,6 +31,7 @@ + - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs index d628bdd..ee23eb4 100644 --- a/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs @@ -1,6 +1,10 @@ +using System; +using System.Diagnostics; +using System.Reflection; using Avalonia.Controls; using Avalonia.Interactivity; using MetaforceInstaller.Core.Intefaces; +using MetaforceInstaller.Core.Models; using MetaforceInstaller.Core.Services; using MetaforceInstaller.UI.ViewModels; @@ -18,6 +22,9 @@ public partial class MainWindow : Window _viewModel = new MainWindowViewModel(); _storageService = new StorageService(); DataContext = _viewModel; + + VersionLabel.Content = Assembly.GetExecutingAssembly() + .GetCustomAttribute()?.Version; NewInstallationButton.Click += OnNewInstalltionClick; @@ -36,4 +43,41 @@ public partial class MainWindow : Window await newInstallationDialog.ShowDialog(this); LoadInstallations(); } + + public async void OnDeleteInstallationClick(object? sender, RoutedEventArgs e) + { + if (sender is Button button && button.DataContext is InstallationData installationData) + { + var name = installationData.Title; + Console.WriteLine($"Delete {name}"); + } + } + + public async void OnLaunchServerClick(object? sender, RoutedEventArgs e) + { + if (sender is Button button && button.DataContext is InstallationData installationData) + { + var exePath = installationData.Parts.WindowsServerPath; + var processInfo = new ProcessStartInfo + { + FileName = exePath, + UseShellExecute = false, + }; + Process.Start(processInfo); + } + } + + public async void OnLaunchAdminClick(object? sender, RoutedEventArgs e) + { + if (sender is Button button && button.DataContext is InstallationData installationData) + { + var exePath = installationData.Parts.WindowsAdminPath; + var processInfo = new ProcessStartInfo + { + FileName = exePath, + UseShellExecute = false, + }; + Process.Start(processInfo); + } + } } \ No newline at end of file From bac6bb9ccd4cb6d7f2dc3a1afc143f7694bc9183 Mon Sep 17 00:00:00 2001 From: shept Date: Fri, 2 Jan 2026 20:57:42 +0500 Subject: [PATCH 09/11] probably fix adb missing dlls --- .../MetaforceInstaller.Core.csproj | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj index 0a85bfc..0b0eeda 100644 --- a/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj +++ b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj @@ -7,20 +7,26 @@ - - - + + + PreserveNewest + AdbWinApi.dll + + + PreserveNewest + AdbWinUsbApi.dll + - - - - - + + + + + - - + + From 864a07c75c08aaf994340c92a7f8fbb48fc05f8b Mon Sep 17 00:00:00 2001 From: shept Date: Fri, 2 Jan 2026 21:49:44 +0500 Subject: [PATCH 10/11] add cool status circle changing depends on adb device connected --- .../Services/AdbService.cs | 59 ++++++++++++++- .../ViewModels/MainWindowViewModel.cs | 37 ++++++++++ .../Windows/MainWindow.axaml | 5 +- .../Windows/MainWindow.axaml.cs | 73 ++++++++++++++++++- 4 files changed, 169 insertions(+), 5 deletions(-) diff --git a/MetaforceInstaller.Core/Services/AdbService.cs b/MetaforceInstaller.Core/Services/AdbService.cs index ddf790f..07ca0ed 100644 --- a/MetaforceInstaller.Core/Services/AdbService.cs +++ b/MetaforceInstaller.Core/Services/AdbService.cs @@ -15,9 +15,15 @@ public class AdbService : IAdbService private readonly ILogger _logger; private readonly AdbClient _adbClient; private DeviceData _deviceData; + private DeviceMonitor? _deviceMonitor; public event EventHandler? ProgressChanged; public event EventHandler? StatusChanged; + public EventHandler? DeviceConnected; + public EventHandler? DeviceDisconnected; + public EventHandler? DeviceChanged; + + public bool IsDeviceConnected => _deviceData != null && _deviceData.State == DeviceState.Online; public AdbService(ILogger? logger = null) { @@ -27,6 +33,46 @@ public class AdbService : IAdbService var serverStatus = server.StartServer(adbPath, restartServerIfNewer: false); _adbClient = new AdbClient(); RefreshDeviceData(); + StartDeviceMonitoring(); + } + + private void StartDeviceMonitoring() + { + try + { + _deviceMonitor = new DeviceMonitor(new AdbSocket(_adbClient.EndPoint)); + _deviceMonitor.DeviceConnected += OnDeviceConnected; + _deviceMonitor.DeviceDisconnected += OnDeviceDisconnected; + _deviceMonitor.DeviceChanged += OnDeviceChanged; + _deviceMonitor.Start(); + + _logger.LogInformation("Device monitoring started"); + } + catch (Exception ex) + { + _logger.LogError($"Failed to start device monitoring: {ex.Message}"); + } + } + + private void OnDeviceConnected(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device conn: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceConnected?.Invoke(this, e); + } + + private void OnDeviceDisconnected(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device disconnected: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceDisconnected?.Invoke(this, e); + } + + private void OnDeviceChanged(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device changed: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceChanged?.Invoke(this, e); } public void RefreshDeviceData() @@ -163,7 +209,7 @@ public class AdbService : IAdbService () => { _adbClient.ExecuteRemoteCommand($"mkdir -p \"{remoteDir}\"", _deviceData, reciever); }, cancellationToken); } - + _logger.LogInformation($"Ensured remote directory: {remoteDir}"); await Task.Run(() => @@ -203,4 +249,15 @@ public class AdbService : IAdbService { return new DeviceInfo(_deviceData.Serial, _deviceData.State.ToString(), _deviceData.Model, _deviceData.Name); } + + public void Dispose() + { + if (_deviceMonitor != null) + { + _deviceMonitor.DeviceConnected -= OnDeviceConnected; + _deviceMonitor.DeviceDisconnected -= OnDeviceDisconnected; + _deviceMonitor.DeviceChanged -= OnDeviceChanged; + _deviceMonitor.Dispose(); + } + } } \ No newline at end of file diff --git a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs index 6a81dcd..6fbef5c 100644 --- a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs +++ b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; +using Avalonia.Media; using MetaforceInstaller.Core.Models; namespace MetaforceInstaller.UI.ViewModels; @@ -12,6 +13,42 @@ public class MainWindowViewModel : INotifyPropertyChanged public ObservableCollection Installations { get; set; } = new(); + private bool _isDeviceConnected; + + public bool IsDeviceConnected + { + get => _isDeviceConnected; + set + { + if (_isDeviceConnected != value) + { + _isDeviceConnected = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(StatusColor)); + OnPropertyChanged(nameof(StatusText)); + } + } + } + + private string _deviceSerial = string.Empty; + + public string DeviceSerial + { + get => _deviceSerial; + set + { + if (_deviceSerial != value) + { + _deviceSerial = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(StatusText)); + } + } + } + + public IBrush StatusColor => IsDeviceConnected ? Brushes.Green : Brushes.Red; + public string StatusText => IsDeviceConnected ? $"Connected to {_deviceSerial}" : "Not connected"; + public void LoadInstallations(IEnumerable data) { Installations.Clear(); diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml index eaaf434..9e4547b 100644 --- a/MetaforceInstaller.UI/Windows/MainWindow.axaml +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml @@ -75,7 +75,10 @@ - \ No newline at end of file diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs index ee23eb4..8ab161b 100644 --- a/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs @@ -1,8 +1,10 @@ using System; using System.Diagnostics; using System.Reflection; +using AdvancedSharpAdbClient.Models; using Avalonia.Controls; using Avalonia.Interactivity; +using Avalonia.Threading; using MetaforceInstaller.Core.Intefaces; using MetaforceInstaller.Core.Models; using MetaforceInstaller.Core.Services; @@ -14,6 +16,7 @@ public partial class MainWindow : Window { private MainWindowViewModel _viewModel; private IStorageService _storageService; + private AdbService _adbService; public MainWindow() { @@ -21,14 +24,68 @@ public partial class MainWindow : Window _viewModel = new MainWindowViewModel(); _storageService = new StorageService(); + _adbService = new AdbService(); + DataContext = _viewModel; - + VersionLabel.Content = Assembly.GetExecutingAssembly() .GetCustomAttribute()?.Version; NewInstallationButton.Click += OnNewInstalltionClick; - + + _adbService.DeviceConnected += OnAdbDeviceConnected; + _adbService.DeviceDisconnected += OnAdbDeviceDisconnected; + _adbService.DeviceChanged += OnAdbDeviceChanged; + LoadInstallations(); + UpdateDeviceStatus(); + } + + private void UpdateDeviceStatus() + { + var isConnected = _adbService.IsDeviceConnected; + _viewModel.IsDeviceConnected = isConnected; + + if (isConnected) + { + try + { + var deviceInfo = _adbService.GetDeviceInfo(); + _viewModel.DeviceSerial = deviceInfo.SerialNumber; + } + catch + { + _viewModel.DeviceSerial = "Unknown"; + } + } + else + { + _viewModel.DeviceSerial = string.Empty; + } + } + + private void OnAdbDeviceConnected(object? sender, DeviceDataEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + UpdateDeviceStatus(); + }); + } + + private void OnAdbDeviceDisconnected(object? sender, DeviceDataEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + UpdateDeviceStatus(); + }); + } + + private void OnAdbDeviceChanged(object? sender, DeviceDataEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + UpdateDeviceStatus(); + }); } private void LoadInstallations() @@ -52,7 +109,7 @@ public partial class MainWindow : Window Console.WriteLine($"Delete {name}"); } } - + public async void OnLaunchServerClick(object? sender, RoutedEventArgs e) { if (sender is Button button && button.DataContext is InstallationData installationData) @@ -80,4 +137,14 @@ public partial class MainWindow : Window Process.Start(processInfo); } } + + protected override void OnClosed(EventArgs e) + { + _adbService.DeviceConnected -= OnAdbDeviceConnected; + _adbService.DeviceDisconnected -= OnAdbDeviceDisconnected; + _adbService.DeviceChanged -= OnAdbDeviceChanged; + _adbService.Dispose(); + + base.OnClosed(e); + } } \ No newline at end of file From ab49284f2fe06d6efb64e71efeee162fb609631b Mon Sep 17 00:00:00 2001 From: shept Date: Sat, 3 Jan 2026 12:23:50 +0500 Subject: [PATCH 11/11] add localization --- MetaforceInstaller.UI/App.axaml.cs | 1 + .../Lang/Resources.Designer.cs | 251 ++++++++++++++++++ MetaforceInstaller.UI/Lang/Resources.resx | 84 ++++++ MetaforceInstaller.UI/Lang/Resources.ru.resx | 77 ++++++ .../MetaforceInstaller.UI.csproj | 15 ++ MetaforceInstaller.UI/TextDefaults.cs | 9 - .../ViewModels/MainWindowViewModel.cs | 2 +- .../Windows/MainWindow.axaml | 39 +-- .../Windows/NewInstallationDialog.axaml | 21 +- .../Windows/NewInstallationDialog.axaml.cs | 26 +- 10 files changed, 475 insertions(+), 50 deletions(-) create mode 100644 MetaforceInstaller.UI/Lang/Resources.Designer.cs create mode 100644 MetaforceInstaller.UI/Lang/Resources.resx create mode 100644 MetaforceInstaller.UI/Lang/Resources.ru.resx delete mode 100644 MetaforceInstaller.UI/TextDefaults.cs diff --git a/MetaforceInstaller.UI/App.axaml.cs b/MetaforceInstaller.UI/App.axaml.cs index fc9da21..38eec17 100644 --- a/MetaforceInstaller.UI/App.axaml.cs +++ b/MetaforceInstaller.UI/App.axaml.cs @@ -14,6 +14,7 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { + Lang.Resources.Culture = System.Globalization.CultureInfo.CurrentCulture; if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow(); diff --git a/MetaforceInstaller.UI/Lang/Resources.Designer.cs b/MetaforceInstaller.UI/Lang/Resources.Designer.cs new file mode 100644 index 0000000..a61e233 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.Designer.cs @@ -0,0 +1,251 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MetaforceInstaller.UI.Lang { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MetaforceInstaller.UI.Lang.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add new installation. + /// + public static string AddInstallation { + get { + return ResourceManager.GetString("AddInstallation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string CancelButton { + get { + return ResourceManager.GetString("CancelButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose .zip. + /// + public static string ChooseZip { + get { + return ResourceManager.GetString("ChooseZip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connected to. + /// + public static string ConnectedTo { + get { + return ResourceManager.GetString("ConnectedTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Delete { + get { + return ResourceManager.GetString("Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install admin. + /// + public static string InstallAdminCheckbox { + get { + return ResourceManager.GetString("InstallAdminCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install Android admin. + /// + public static string InstallAndroidAdmin { + get { + return ResourceManager.GetString("InstallAndroidAdmin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install. + /// + public static string InstallButton { + get { + return ResourceManager.GetString("InstallButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install server. + /// + public static string InstallServerCheckbox { + get { + return ResourceManager.GetString("InstallServerCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install VR client. + /// + public static string InstallVRClient { + get { + return ResourceManager.GetString("InstallVRClient", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch PC admin. + /// + public static string LaunchPCAdmin { + get { + return ResourceManager.GetString("LaunchPCAdmin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch server. + /// + public static string LaunchServer { + get { + return ResourceManager.GetString("LaunchServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name of new installation. + /// + public static string NameOfNewInstallation { + get { + return ResourceManager.GetString("NameOfNewInstallation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find Android content. + /// + public static string NoAndroidContentError { + get { + return ResourceManager.GetString("NoAndroidContentError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find directory with PC admin. + /// + public static string NoPCAdminError { + get { + return ResourceManager.GetString("NoPCAdminError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find directory with server. + /// + public static string NoServerError { + get { + return ResourceManager.GetString("NoServerError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not connected. + /// + public static string NotConnected { + get { + return ResourceManager.GetString("NotConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find any VR clients. + /// + public static string NoVRClientsError { + get { + return ResourceManager.GetString("NoVRClientsError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find Windows content. + /// + public static string NoWindowsContentError { + get { + return ResourceManager.GetString("NoWindowsContentError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save Android admin. + /// + public static string SaveAndroidAdminCheckbox { + get { + return ResourceManager.GetString("SaveAndroidAdminCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save VR client. + /// + public static string SaveVRClientCheckbox { + get { + return ResourceManager.GetString("SaveVRClientCheckbox", resourceCulture); + } + } + } +} diff --git a/MetaforceInstaller.UI/Lang/Resources.resx b/MetaforceInstaller.UI/Lang/Resources.resx new file mode 100644 index 0000000..c641fa3 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.resx @@ -0,0 +1,84 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Launch server + + + Launch PC admin + + + Install VR client + + + Install Android admin + + + Delete + + + Add new installation + + + Not connected + + + Connected to + + + Name of new installation + + + Choose .zip + + + Install server + + + Install admin + + + Save Android admin + + + Save VR client + + + Install + + + Cancel + + + Couldn't find directory with server + + + Couldn't find directory with PC admin + + + Couldn't find Windows content + + + Couldn't find Android content + + + Couldn't find any VR clients + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/Lang/Resources.ru.resx b/MetaforceInstaller.UI/Lang/Resources.ru.resx new file mode 100644 index 0000000..891e8d9 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.ru.resx @@ -0,0 +1,77 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Запустить сервер + + + Запустить админку + + + Удалить + + + Установить андроид-админку + + + Установить на шлем + + + Добавить версию + + + Подключено: + + + Не подключено + + + Выбрать .zip + + + Отмена + + + Установить админку + + + Установить + + + Установить сервер + + + Название версии + + + Сохранить андроид-админку + + + Сохранить VR-клиенты + + + Не удалось обнаружить Андроид контент + + + Не удалось обнаружить ПК-админку + + + Не удалось обнаружить сервер + + + Не удалось обнаружить VR-клиенты + + + Не удалось обнаружить Windows контент + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj index ad61469..3a83cb5 100644 --- a/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj +++ b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj @@ -34,4 +34,19 @@ + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + diff --git a/MetaforceInstaller.UI/TextDefaults.cs b/MetaforceInstaller.UI/TextDefaults.cs deleted file mode 100644 index 176e1f6..0000000 --- a/MetaforceInstaller.UI/TextDefaults.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MetaforceInstaller.UI; - -public static class TextDefaults -{ - public static readonly string InstallServer = "Install Server"; - public static readonly string InstallAdmin = "Install Admin"; - public static readonly string SaveAndroidAdmin = "Save Android admin"; - public static readonly string SaveVRClient = "Save VR client"; -} \ No newline at end of file diff --git a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs index 6fbef5c..22c0005 100644 --- a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs +++ b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs @@ -47,7 +47,7 @@ public class MainWindowViewModel : INotifyPropertyChanged } public IBrush StatusColor => IsDeviceConnected ? Brushes.Green : Brushes.Red; - public string StatusText => IsDeviceConnected ? $"Connected to {_deviceSerial}" : "Not connected"; + public string StatusText => IsDeviceConnected ? $"{Lang.Resources.ConnectedTo} {_deviceSerial}" : Lang.Resources.NotConnected; public void LoadInstallations(IEnumerable data) { diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml index 9e4547b..e7d4655 100644 --- a/MetaforceInstaller.UI/Windows/MainWindow.axaml +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:MetaforceInstaller.UI.ViewModels" + xmlns:lang="using:MetaforceInstaller.UI.Lang" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:CompileBindings="True" x:DataType="vm:MainWindowViewModel" @@ -43,7 +44,12 @@ @@ -51,23 +57,22 @@ - - - - - - - + diff --git a/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml index 603e651..d9a410a 100644 --- a/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml +++ b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:lang="clr-namespace:MetaforceInstaller.UI.Lang" mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="400" x:Class="MetaforceInstaller.UI.Windows.NewInstallationDialog" Title="MetaforceInstaller - Add new installation" @@ -9,19 +10,19 @@ CanResize="False"> - - - - Install server - Install admin - Save android admin - Save VR client +