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/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/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 + - - - - - + + + + + - - + + 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/Models/InstallationData.cs b/MetaforceInstaller.Core/Models/InstallationData.cs new file mode 100644 index 0000000..6f53baa --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationData.cs @@ -0,0 +1,9 @@ +namespace MetaforceInstaller.Core.Models; + +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 new file mode 100644 index 0000000..62acb62 --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -0,0 +1,12 @@ +namespace MetaforceInstaller.Core.Models; + +public class InstallationParts +{ + 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/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.Core/Services/StorageService.cs b/MetaforceInstaller.Core/Services/StorageService.cs new file mode 100644 index 0000000..4ab4ffa --- /dev/null +++ b/MetaforceInstaller.Core/Services/StorageService.cs @@ -0,0 +1,35 @@ +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 appDirectory = Defaults.StoragePath; + 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 diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs new file mode 100644 index 0000000..b5c4122 --- /dev/null +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -0,0 +1,160 @@ +using System.IO.Compression; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public class ZipScrapper +{ + public static InstallationParts PeekFiles(ZipArchive archive) + { + return new InstallationParts + { + AndroidContentPath = FindAndroidContent(archive), + OculusClientPath = FindOculusClient(archive), + PicoClientPath = FindPicoClient(archive), + AndroidAdminPath = FindAndroidAdmin(archive), + WindowsAdminPath = FindPcAdmin(archive), + WindowsContentPath = FindWindowsContent(archive), + WindowsServerPath = FindServer(archive), + }; + } + + /// + /// 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(installationFolder, entry.FullName); + var destinationDir = Path.GetDirectoryName(destinationPath); + + if (!string.IsNullOrEmpty(destinationDir)) + { + Directory.CreateDirectory(destinationDir); + } + + entry.ExtractToFile(destinationPath, overwrite: true); + + processedEntries++; + progress?.Report((double)processedEntries / totalEntries * 100); + } + + return installationFolder; + } + + /// + /// Updates InstallationParts paths to reflect the actual extracted location + /// + public static InstallationParts UpdatePathsAfterExtraction( + InstallationParts parts, + string extractedFolderPath) + { + 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; + } + + /// + /// Finds an executable in archive, excluding crash handlers + /// + private static string? FindExecutable(ZipArchive archive, string containsName) + { + 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..38eec17 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; @@ -13,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/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml deleted file mode 100644 index 61a426d..0000000 --- a/MetaforceInstaller.UI/MainWindow.axaml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + +