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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/MainWindow.axaml.cs b/MetaforceInstaller.UI/MainWindow.axaml.cs
deleted file mode 100644
index d6a49e1..0000000
--- a/MetaforceInstaller.UI/MainWindow.axaml.cs
+++ /dev/null
@@ -1,230 +0,0 @@
-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.Services;
-
-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;
-
- public MainWindow()
- {
- InitializeComponent();
-
- LogMessage("MetaforceInstaller by slavagm");
-
- _adbService = new AdbService();
- _adbService.ProgressChanged += OnAdbProgressChanged;
- _adbService.StatusChanged += OnAdbStatusChanged;
-
- CheckAndEnableInstallButton();
-
- ChooseApkButton.Click += OnChooseApkClicked;
- ChooseContentButton.Click += OnChooseContentClicked;
- InstallButton.Click += OnInstallClicked;
- }
-
- 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/MetaforceInstaller.UI.csproj b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj
index 26ffaf3..3a83cb5 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
@@ -34,4 +34,19 @@
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
diff --git a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..22c0005
--- /dev/null
+++ b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,65 @@
+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;
+
+public class MainWindowViewModel : INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ 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 ? $"{Lang.Resources.ConnectedTo} {_deviceSerial}" : Lang.Resources.NotConnected;
+
+ 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
diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml
new file mode 100644
index 0000000..e7d4655
--- /dev/null
+++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs
new file mode 100644
index 0000000..8ab161b
--- /dev/null
+++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml.cs
@@ -0,0 +1,150 @@
+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;
+using MetaforceInstaller.UI.ViewModels;
+
+namespace MetaforceInstaller.UI.Windows;
+
+public partial class MainWindow : Window
+{
+ private MainWindowViewModel _viewModel;
+ private IStorageService _storageService;
+ private AdbService _adbService;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ _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()
+ {
+ var appData = _storageService.Load();
+ _viewModel.LoadInstallations(appData.Installations);
+ }
+
+ public async void OnNewInstalltionClick(object? sender, RoutedEventArgs e)
+ {
+ var newInstallationDialog = new NewInstallationDialog(_storageService);
+ 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);
+ }
+ }
+
+ 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
diff --git a/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml
new file mode 100644
index 0000000..d9a410a
--- /dev/null
+++ b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs
new file mode 100644
index 0000000..da8b7a1
--- /dev/null
+++ b/MetaforceInstaller.UI/Windows/NewInstallationDialog.axaml.cs
@@ -0,0 +1,164 @@
+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.Intefaces;
+using MetaforceInstaller.Core.Models;
+using MetaforceInstaller.Core.Services;
+
+namespace MetaforceInstaller.UI.Windows;
+
+public partial class NewInstallationDialog : Window
+{
+ private string? _zipPath;
+ private InstallationParts? _installationParts;
+ private readonly IStorageService _storageService;
+
+ public NewInstallationDialog(IStorageService storageService)
+ {
+ InitializeComponent();
+
+ _storageService = storageService;
+
+ RefreshCheckboxes();
+ CancelButton.Click += OnCancelClick;
+ ChooseZip.Click += OnChooseZipClick;
+ InstallButton.IsEnabled = false;
+ InstallButton.Click += OnInstallClick;
+ }
+
+ private void RefreshCheckboxes()
+ {
+ var serverCheckbox = ServerCheckBox;
+ var pcAdminCheckbox = PcAdminCheckBox;
+ var androidAdminCheckbox = AndroidAdminCheckbox;
+ var vrClientCheckbox = VrClientCheckbox;
+ serverCheckbox.Content = Lang.Resources.InstallServerCheckbox;
+ serverCheckbox.IsEnabled = true;
+ pcAdminCheckbox.Content = Lang.Resources.InstallAdminCheckbox;
+ pcAdminCheckbox.IsEnabled = true;
+ androidAdminCheckbox.Content = Lang.Resources.SaveAndroidAdminCheckbox;
+ androidAdminCheckbox.IsEnabled = true;
+ vrClientCheckbox.Content = Lang.Resources.SaveVRClientCheckbox;
+ 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);
+ 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(() =>
+ {
+ extractedPath = ZipScrapper.ExtractZip(
+ archive,
+ Defaults.StoragePath,
+ installationGuid,
+ progress);
+ });
+
+ InstallButton.IsEnabled = false;
+
+ var appData = _storageService.Load();
+
+ var updatedParts = ZipScrapper.UpdatePathsAfterExtraction(_installationParts, extractedPath);
+
+ var installationData = new InstallationData
+ {
+ Id = installationGuid,
+ Title = title,
+ Parts = updatedParts
+ };
+
+ appData.Installations.Add(installationData);
+ _storageService.Save(appData);
+ Close();
+ }
+
+ private void UpdateCheckboxes()
+ {
+ RefreshCheckboxes();
+ var serverCheckbox = ServerCheckBox;
+ var pcAdminCheckbox = PcAdminCheckBox;
+ var androidAdminCheckbox = AndroidAdminCheckbox;
+ var vrClientCheckbox = VrClientCheckbox;
+
+
+ if (string.IsNullOrEmpty(_installationParts.WindowsServerPath))
+ {
+ serverCheckbox.IsEnabled = false;
+ serverCheckbox.Content += $"\n{Lang.Resources.NoServerError}";
+ }
+
+ if (string.IsNullOrEmpty(_installationParts.WindowsAdminPath))
+ {
+ pcAdminCheckbox.IsEnabled = false;
+ pcAdminCheckbox.Content += $"\n{Lang.Resources.NoPCAdminError}";
+ }
+
+ if (string.IsNullOrEmpty(_installationParts.WindowsContentPath))
+ {
+ pcAdminCheckbox.IsEnabled = false;
+ pcAdminCheckbox.Content += $"\n{Lang.Resources.NoWindowsContentError}";
+ }
+
+ if (string.IsNullOrEmpty(_installationParts.AndroidContentPath))
+ {
+ vrClientCheckbox.IsEnabled = false;
+ vrClientCheckbox.Content += $"\n{Lang.Resources.NoAndroidContentError}";
+ androidAdminCheckbox.IsEnabled = false;
+ androidAdminCheckbox.Content += $"\n{Lang.Resources.NoAndroidContentError}";
+ }
+
+ if (string.IsNullOrEmpty(_installationParts.PicoClientPath) &&
+ string.IsNullOrEmpty(_installationParts.OculusClientPath))
+ {
+ vrClientCheckbox.IsEnabled = false;
+ vrClientCheckbox.Content += $"\n{Lang.Resources.NoVRClientsError}";
+ }
+
+ InstallButton.IsEnabled = new List
+ { serverCheckbox, pcAdminCheckbox, androidAdminCheckbox, vrClientCheckbox }.Any(x => x?.IsEnabled == true);
+ }
+
+ private void OnCancelClick(object? sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+}
\ No newline at end of file