diff --git a/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj
index 79e2376..dee32c2 100644
--- a/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj
+++ b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj
@@ -7,14 +7,16 @@
enable
+
+
+
+
-
+
-
-
-
+
diff --git a/MetaforceInstaller.Cli/Program.cs b/MetaforceInstaller.Cli/Program.cs
index fa8c389..bf8bf51 100644
--- a/MetaforceInstaller.Cli/Program.cs
+++ b/MetaforceInstaller.Cli/Program.cs
@@ -1,57 +1,54 @@
-using System.Reflection;
-using AdvancedSharpAdbClient;
-using AdvancedSharpAdbClient.DeviceCommands;
-using AdvancedSharpAdbClient.Models;
+using MetaforceInstaller.Cli.Utils;
+using MetaforceInstaller.Core.Services;
namespace MetaforceInstaller.Cli;
-class Program
+static class Program
{
- // 1. Получить имя апк и зипки, если не предоставлены - забить дефолтными значениями
- // 2. Распаковать в временную директорию adb (готово)
- // 3. Установить апк
- // 4. Получить имя пакета
- // 5. Сформировать строку пути для контента
- // 6. Копировать зип по сформированному пути
-
- static AdbClient adbClient;
- static DeviceData deviceData;
-
- static void Main(string[] args)
+ static async Task Main(string[] args)
{
try
{
- var (apkPath, zipPath, outputPath) = ParseArguments(args);
+ var installationRequest = ArgumentParser.ParseArguments(args);
- if (string.IsNullOrEmpty(apkPath) || string.IsNullOrEmpty(zipPath) || string.IsNullOrEmpty(outputPath))
+ if (installationRequest is null ||
+ string.IsNullOrEmpty(installationRequest.ApkPath) ||
+ string.IsNullOrEmpty(installationRequest.ZipPath))
{
ShowUsage();
return;
}
- var adbPath = ExtractAdbFiles();
+ var adbService = new AdbService();
- var server = new AdbServer();
- var result = server.StartServer(adbPath, restartServerIfNewer: false);
- Console.WriteLine($"ADB сервер запущен: {result}");
+ var apkInfo = ApkScrapper.GetApkInfo(installationRequest.ApkPath);
+ var zipName = Path.GetFileName(installationRequest.ZipPath);
+ var outputPath =
+ @$"/storage/emulated/0/Android/data/{apkInfo.PackageName}/files/{zipName}";
- adbClient = new AdbClient();
+ // Подписка на события прогресса
+ adbService.ProgressChanged += OnProgressChanged;
+ adbService.StatusChanged += OnStatusChanged;
- var devices = adbClient.GetDevices();
+ // Получение информации об устройстве
+ var deviceInfo = adbService.GetDeviceInfo();
+ Console.WriteLine($"Найдено устройство: {deviceInfo.SerialNumber}");
+ Console.WriteLine($"Состояние: {deviceInfo.State}");
+ Console.WriteLine($"Модель: {deviceInfo.Model} - {deviceInfo.Name}");
+ Console.WriteLine();
- if (!devices.Any())
- {
- Console.WriteLine("Устройства не найдены. Подключите Android-устройство и включите отладку по USB.");
- return;
- }
+ // Создание объекта для отслеживания прогресса
+ var progress = new Progress(OnProgressReport);
- deviceData = devices.FirstOrDefault();
- Console.WriteLine($"Найдено устройство: {deviceData.Serial}");
- Console.WriteLine($"Состояние: {deviceData.State}");
- Console.WriteLine($"Имя устройства: {deviceData.Name} - {deviceData.Model}");
+ // Установка APK
+ await adbService.InstallApkAsync(installationRequest.ApkPath, progress);
+ Console.WriteLine();
- InstallApk(apkPath);
- CopyFileToDevice(zipPath, outputPath);
+ // Копирование файла
+ await adbService.CopyFileAsync(installationRequest.ZipPath, outputPath, progress);
+ Console.WriteLine();
+
+ Console.WriteLine("Операция завершена успешно!");
}
catch (Exception ex)
{
@@ -59,6 +56,29 @@ class Program
}
}
+ private static void OnProgressChanged(object? sender, MetaforceInstaller.Core.Models.ProgressInfo e)
+ {
+ DrawProgressBar(e.PercentageComplete, e.BytesTransferred, e.TotalBytes);
+ }
+
+ private static void OnStatusChanged(object? sender, string e)
+ {
+ Console.WriteLine(e);
+ }
+
+ private static void OnProgressReport(MetaforceInstaller.Core.Models.ProgressInfo progressInfo)
+ {
+ if (progressInfo.TotalBytes > 0)
+ {
+ DrawProgressBar(progressInfo.PercentageComplete, progressInfo.BytesTransferred, progressInfo.TotalBytes);
+ }
+ else
+ {
+ // Для случаев без информации о байтах (например, установка APK)
+ DrawProgressBar(progressInfo.PercentageComplete, 0, 100);
+ }
+ }
+
static void ShowUsage()
{
Console.WriteLine("Использование:");
@@ -78,168 +98,36 @@ class Program
Console.WriteLine(" MetaforceInstaller.exe -a app.apk -c data.zip -o /sdcard/data.zip");
}
-
- private static (string? apkPath, string? zipPath, string? outputPath) ParseArguments(string[] args)
+ private static void DrawProgressBar(int progress, long receivedBytes, long totalBytes)
{
- string apkPath = null;
- string zipPath = null;
- string outputPath = null;
+ Console.SetCursorPosition(0, Console.CursorTop);
- for (int i = 0; i < args.Length; i++)
+ var barLength = 40;
+ var filledLength = (int)(barLength * progress / 100.0);
+
+ var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]";
+
+ string bytesText = "";
+ if (totalBytes > 0)
{
- switch (args[i].ToLower())
- {
- case "--apk":
- case "-a":
- if (i + 1 < args.Length)
- {
- apkPath = args[i + 1];
- i++;
- }
-
- break;
-
- case "--content":
- case "-c":
- if (i + 1 < args.Length)
- {
- zipPath = args[i + 1];
- i++;
- }
-
- break;
-
- case "--output":
- case "-o":
- if (i + 1 < args.Length)
- {
- outputPath = args[i + 1];
- i++;
- }
-
- break;
-
- case "--help":
- case "-h":
- ShowUsage();
- Environment.Exit(0);
- break;
- }
+ bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}";
}
- return (apkPath, zipPath, outputPath);
+ Console.Write($"\r{bar} {progress}%{bytesText}");
}
- private static void ExtractResource(string resourceName, string outputPath)
+ private static string FormatBytes(long bytes)
{
- using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
- using var fileStream = File.Create(outputPath);
- stream.CopyTo(fileStream);
- }
+ string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
+ var counter = 0;
+ double number = bytes;
- private static string ExtractAdbFiles()
- {
- var tempDir = Path.Combine(Path.GetTempPath(), "MetaforceInstaller", "adb");
- Directory.CreateDirectory(tempDir);
-
- var adbPath = Path.Combine(tempDir, "adb.exe");
-
- if (!File.Exists(adbPath))
+ while (Math.Round(number / 1024) >= 1)
{
- ExtractResource("MetaforceInstaller.Cli.adb.adb.exe", adbPath);
- ExtractResource("MetaforceInstaller.Cli.adb.AdbWinApi.dll", Path.Combine(tempDir, "AdbWinApi.dll"));
- ExtractResource("MetaforceInstaller.Cli.adb.AdbWinUsbApi.dll", Path.Combine(tempDir, "AdbWinUsbApi.dll"));
+ number /= 1024;
+ counter++;
}
- return adbPath;
+ return $"{number:N1} {suffixes[counter]}";
}
-
- private static void InstallApk(string apkPath)
- {
- try
- {
- if (!File.Exists(apkPath))
- {
- Console.WriteLine($"APK файл не найден: {apkPath}");
- return;
- }
-
- Console.WriteLine($"Установка APK: {apkPath}");
-
- var packageManager = new PackageManager(adbClient, deviceData);
- packageManager.InstallPackage(apkPath, new Action(o => { }));
-
- Console.WriteLine("APK успешно установлен!");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Ошибка установки APK: {ex.Message}");
- }
- }
-
-private static void CopyFileToDevice(string localPath, string remotePath)
-{
- try
- {
- if (!File.Exists(localPath))
- {
- Console.WriteLine($"Локальный файл не найден: {localPath}");
- return;
- }
-
- Console.WriteLine($"Копирование файла {localPath} в {remotePath}");
-
- var lastProgress = -1;
-
- using var fileStream = File.OpenRead(localPath);
- var syncService = new SyncService(adbClient, deviceData);
-
- syncService.Push(fileStream, remotePath, UnixFileStatus.DefaultFileMode, DateTime.Now,
- new Action(progress =>
- {
- var currentProgress = progress.ProgressPercentage;
-
- if (currentProgress != lastProgress)
- {
- lastProgress = (int)currentProgress;
- DrawProgressBar(lastProgress, progress.ReceivedBytesSize, progress.TotalBytesToReceive);
- }
- }));
-
- Console.WriteLine();
- Console.WriteLine("Файл успешно скопирован!");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Ошибка копирования файла: {ex.Message}");
- }
-}
-
-private static void DrawProgressBar(int progress, long receivedBytes, long totalBytes)
-{
- Console.SetCursorPosition(0, Console.CursorTop);
-
- var barLength = 40;
- var filledLength = (int)(barLength * progress / 100.0);
-
- var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]";
- var bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}";
-
- Console.Write($"\r{bar} {progress}%{bytesText}");
-}
-
-private static 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]}";
-}
}
\ No newline at end of file
diff --git a/MetaforceInstaller.Cli/Utils/ArgumentParser.cs b/MetaforceInstaller.Cli/Utils/ArgumentParser.cs
new file mode 100644
index 0000000..ffdb561
--- /dev/null
+++ b/MetaforceInstaller.Cli/Utils/ArgumentParser.cs
@@ -0,0 +1,53 @@
+using MetaforceInstaller.Core.Models;
+
+namespace MetaforceInstaller.Cli.Utils;
+
+public static class ArgumentParser
+{
+ public static InstallationRequest? ParseArguments(string[] args)
+ {
+ var result = new InstallationRequest();
+
+ for (var i = 0; i < args.Length; i++)
+ {
+ switch (args[i].ToLower())
+ {
+ case "--apk":
+ case "-a":
+ if (i + 1 < args.Length)
+ {
+ result.ApkPath = args[i + 1];
+ i++;
+ }
+
+ break;
+
+ case "--content":
+ case "-c":
+ if (i + 1 < args.Length)
+ {
+ result.ZipPath = args[i + 1];
+ i++;
+ }
+
+ break;
+
+ case "--output":
+ case "-o":
+ if (i + 1 < args.Length)
+ {
+ result.OutputPath = args[i + 1];
+ i++;
+ }
+
+ break;
+
+ case "--help":
+ case "-h":
+ return null;
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/Intefaces/IAdbService.cs b/MetaforceInstaller.Core/Intefaces/IAdbService.cs
new file mode 100644
index 0000000..444c237
--- /dev/null
+++ b/MetaforceInstaller.Core/Intefaces/IAdbService.cs
@@ -0,0 +1,18 @@
+using MetaforceInstaller.Core.Models;
+using MetaforceInstaller.Core.Models;
+
+namespace MetaforceInstaller.Core.Intefaces;
+
+public interface IAdbService
+{
+ event EventHandler? ProgressChanged;
+ event EventHandler? StatusChanged;
+
+ Task InstallApkAsync(string apkPath, IProgress? progress = null, CancellationToken cancellationToken = default);
+ Task CopyFileAsync(string localPath, string remotePath, IProgress? progress = null, CancellationToken cancellationToken = default);
+ DeviceInfo GetDeviceInfo();
+
+ // Синхронные версии для обратной совместимости
+ void InstallApk(string apkPath);
+ void CopyFile(string localPath, string remotePath);
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj
new file mode 100644
index 0000000..0a85bfc
--- /dev/null
+++ b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MetaforceInstaller.Core/Models/ApkInfo.cs b/MetaforceInstaller.Core/Models/ApkInfo.cs
new file mode 100644
index 0000000..9f042bf
--- /dev/null
+++ b/MetaforceInstaller.Core/Models/ApkInfo.cs
@@ -0,0 +1,3 @@
+namespace MetaforceInstaller.Core.Models;
+
+public record ApkInfo(string PackageName, string VersionName, string VersionCode);
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/Models/DeviceInfo.cs b/MetaforceInstaller.Core/Models/DeviceInfo.cs
new file mode 100644
index 0000000..802cd3c
--- /dev/null
+++ b/MetaforceInstaller.Core/Models/DeviceInfo.cs
@@ -0,0 +1,8 @@
+namespace MetaforceInstaller.Core.Models;
+
+public record DeviceInfo(
+ string SerialNumber,
+ string State,
+ string Model,
+ string Name
+);
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/Models/InstallationRequest.cs b/MetaforceInstaller.Core/Models/InstallationRequest.cs
new file mode 100644
index 0000000..c64b271
--- /dev/null
+++ b/MetaforceInstaller.Core/Models/InstallationRequest.cs
@@ -0,0 +1,8 @@
+namespace MetaforceInstaller.Core.Models;
+
+public class InstallationRequest
+{
+ public string ApkPath { get; set; }
+ public string ZipPath { get; set; }
+ public string OutputPath { get; set; }
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/Models/ProgressInfo.cs b/MetaforceInstaller.Core/Models/ProgressInfo.cs
new file mode 100644
index 0000000..cc09a74
--- /dev/null
+++ b/MetaforceInstaller.Core/Models/ProgressInfo.cs
@@ -0,0 +1,19 @@
+namespace MetaforceInstaller.Core.Models;
+
+public class ProgressInfo
+{
+ public int PercentageComplete { get; set; }
+ public long BytesTransferred { get; set; }
+ public long TotalBytes { get; set; }
+ public string? Message { get; set; }
+ public string? CurrentFile { get; set; }
+ public ProgressType Type { get; set; }
+}
+
+public enum ProgressType
+{
+ Installation,
+ FileCopy,
+ Extraction,
+ General
+}
diff --git a/MetaforceInstaller.Core/Services/AdbService.cs b/MetaforceInstaller.Core/Services/AdbService.cs
new file mode 100644
index 0000000..e0e4920
--- /dev/null
+++ b/MetaforceInstaller.Core/Services/AdbService.cs
@@ -0,0 +1,194 @@
+using System.Reflection;
+using AdvancedSharpAdbClient;
+using AdvancedSharpAdbClient.DeviceCommands;
+using AdvancedSharpAdbClient.Models;
+using MetaforceInstaller.Core.Intefaces;
+using MetaforceInstaller.Core.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace MetaforceInstaller.Core.Services;
+
+public class AdbService : IAdbService
+{
+ private readonly ILogger _logger;
+ private readonly AdbClient _adbClient;
+ private DeviceData _deviceData;
+
+ public event EventHandler? ProgressChanged;
+ public event EventHandler? StatusChanged;
+
+ public AdbService(ILogger? logger = null)
+ {
+ _logger = logger ?? new NullLogger();
+ var adbPath = GetAdbPath();
+ var server = new AdbServer();
+ var serverStatus = server.StartServer(adbPath, restartServerIfNewer: false);
+ _adbClient = new AdbClient();
+ RefreshDeviceData();
+ }
+
+ public void RefreshDeviceData()
+ {
+ var devices = _adbClient.GetDevices();
+ _deviceData = devices.FirstOrDefault();
+ }
+
+ private void ExtractResource(string resourceName, string outputPath)
+ {
+ _logger.LogInformation($"Extracting resource: {resourceName} to {outputPath}");
+ using var stream = Assembly.GetAssembly(typeof(AdbService)).GetManifestResourceStream(resourceName);
+ using var fileStream = File.Create(outputPath);
+ stream.CopyTo(fileStream);
+ _logger.LogInformation($"Resource extracted: {resourceName} to {outputPath}");
+ }
+
+ private string GetAdbPath()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), "MetaforceInstaller", "adb");
+ Directory.CreateDirectory(tempDir);
+
+ var adbPath = Path.Combine(tempDir, "adb.exe");
+
+ if (File.Exists(adbPath)) return adbPath;
+ ExtractResource("MetaforceInstaller.Core.adb.adb.exe", adbPath);
+ ExtractResource("MetaforceInstaller.Core.adb.AdbWinApi.dll", Path.Combine(tempDir, "AdbWinApi.dll"));
+ ExtractResource("MetaforceInstaller.Core.adb.AdbWinUsbApi.dll", Path.Combine(tempDir, "AdbWinUsbApi.dll"));
+
+ return adbPath;
+ }
+
+ private void OnProgressChanged(ProgressInfo progressInfo)
+ {
+ ProgressChanged?.Invoke(this, progressInfo);
+ }
+
+ private void OnStatusChanged(string status)
+ {
+ StatusChanged?.Invoke(this, status);
+ }
+
+ public void InstallApk(string apkPath)
+ {
+ InstallApkAsync(apkPath).Wait();
+ }
+
+ public async Task InstallApkAsync(string apkPath, IProgress? progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ if (!File.Exists(apkPath))
+ {
+ _logger.LogCritical("Error: Could not find APK file.");
+ return;
+ }
+
+ OnStatusChanged("Начинаем установку APK...");
+ _logger.LogInformation($"Installing APK: {apkPath}");
+
+ progress?.Report(new ProgressInfo
+ {
+ PercentageComplete = 0,
+ Message = "Подготовка к установке APK...",
+ Type = ProgressType.Installation,
+ CurrentFile = Path.GetFileName(apkPath)
+ });
+
+ var packageManager = new PackageManager(_adbClient, _deviceData);
+
+ await Task.Run(() =>
+ {
+ packageManager.InstallPackage(apkPath, installProgress =>
+ {
+ var progressInfo = new ProgressInfo
+ {
+ PercentageComplete = (int)installProgress.UploadProgress,
+ Message = $"Установка APK: {installProgress.UploadProgress:F1}%",
+ Type = ProgressType.Installation,
+ CurrentFile = Path.GetFileName(apkPath)
+ };
+
+ progress?.Report(progressInfo);
+ OnProgressChanged(progressInfo);
+ });
+ }, cancellationToken);
+
+ OnStatusChanged("APK успешно установлен!");
+ _logger.LogInformation("APK successfully installed!");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogCritical($"Error: {ex.Message}");
+ throw;
+ }
+ }
+
+ public void CopyFile(string localPath, string remotePath)
+ {
+ CopyFileAsync(localPath, remotePath).Wait();
+ }
+
+ public async Task CopyFileAsync(string localPath, string remotePath, IProgress? progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ if (!File.Exists(localPath))
+ {
+ _logger.LogCritical($"Error: Could not find file: {localPath}");
+ return;
+ }
+
+ OnStatusChanged("Начинаем копирование файла...");
+ _logger.LogInformation($"Copying file: {localPath} to {remotePath}");
+
+ var fileInfo = new FileInfo(localPath);
+
+ progress?.Report(new ProgressInfo
+ {
+ PercentageComplete = 0,
+ Message = "Подготовка к копированию файла...",
+ Type = ProgressType.FileCopy,
+ CurrentFile = Path.GetFileName(localPath),
+ TotalBytes = fileInfo.Length
+ });
+
+ await Task.Run(() =>
+ {
+ using var fileStream = File.OpenRead(localPath);
+ var syncService = new SyncService(_adbClient, _deviceData);
+
+ syncService.Push(fileStream, remotePath, UnixFileStatus.DefaultFileMode, DateTime.Now,
+ copyProgress =>
+ {
+ var progressInfo = new ProgressInfo
+ {
+ PercentageComplete = (int)copyProgress.ProgressPercentage,
+ BytesTransferred = copyProgress.ReceivedBytesSize,
+ TotalBytes = copyProgress.TotalBytesToReceive,
+ Message = $"Копирование: {copyProgress.ProgressPercentage:F1}%",
+ Type = ProgressType.FileCopy,
+ CurrentFile = Path.GetFileName(localPath)
+ };
+
+ progress?.Report(progressInfo);
+ OnProgressChanged(progressInfo);
+ });
+ }, cancellationToken);
+
+ OnStatusChanged("Файл успешно скопирован!");
+ _logger.LogInformation("File successfully copied!");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogCritical($"Error: {ex.Message}");
+ throw;
+ }
+ }
+
+ public DeviceInfo GetDeviceInfo()
+ {
+ return new DeviceInfo(_deviceData.Serial, _deviceData.State.ToString(), _deviceData.Model, _deviceData.Name);
+ }
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.Core/Services/ApkScrapper.cs b/MetaforceInstaller.Core/Services/ApkScrapper.cs
new file mode 100644
index 0000000..c06c543
--- /dev/null
+++ b/MetaforceInstaller.Core/Services/ApkScrapper.cs
@@ -0,0 +1,19 @@
+using AlphaOmega.Debug;
+using MetaforceInstaller.Core.Models;
+
+namespace MetaforceInstaller.Core.Services;
+
+public static class ApkScrapper
+{
+ public static ApkInfo GetApkInfo(string apkPath)
+ {
+ using var apk = new ApkFile(apkPath);
+ if (apk is { IsValid: true, AndroidManifest: not null })
+ {
+ return new ApkInfo(apk.AndroidManifest.Package, apk.AndroidManifest.VersionName,
+ apk.AndroidManifest.VersionCode);
+ }
+
+ throw new Exception("Invalid APK file");
+ }
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.Cli/adb/AdbWinApi.dll b/MetaforceInstaller.Core/adb/AdbWinApi.dll
similarity index 100%
rename from MetaforceInstaller.Cli/adb/AdbWinApi.dll
rename to MetaforceInstaller.Core/adb/AdbWinApi.dll
diff --git a/MetaforceInstaller.Cli/adb/AdbWinUsbApi.dll b/MetaforceInstaller.Core/adb/AdbWinUsbApi.dll
similarity index 100%
rename from MetaforceInstaller.Cli/adb/AdbWinUsbApi.dll
rename to MetaforceInstaller.Core/adb/AdbWinUsbApi.dll
diff --git a/MetaforceInstaller.Cli/adb/adb.exe b/MetaforceInstaller.Core/adb/adb.exe
similarity index 100%
rename from MetaforceInstaller.Cli/adb/adb.exe
rename to MetaforceInstaller.Core/adb/adb.exe
diff --git a/MetaforceInstaller.UI/App.axaml b/MetaforceInstaller.UI/App.axaml
new file mode 100644
index 0000000..d947dd9
--- /dev/null
+++ b/MetaforceInstaller.UI/App.axaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/App.axaml.cs b/MetaforceInstaller.UI/App.axaml.cs
new file mode 100644
index 0000000..16212ac
--- /dev/null
+++ b/MetaforceInstaller.UI/App.axaml.cs
@@ -0,0 +1,23 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace MetaforceInstaller.UI;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml
new file mode 100644
index 0000000..c1e1044
--- /dev/null
+++ b/MetaforceInstaller.UI/MainWindow.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MetaforceInstaller.UI/MainWindow.axaml.cs b/MetaforceInstaller.UI/MainWindow.axaml.cs
new file mode 100644
index 0000000..4990d10
--- /dev/null
+++ b/MetaforceInstaller.UI/MainWindow.axaml.cs
@@ -0,0 +1,230 @@
+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("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
new file mode 100644
index 0000000..34848bc
--- /dev/null
+++ b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj
@@ -0,0 +1,26 @@
+
+
+ WinExe
+ net8.0
+ enable
+ true
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
diff --git a/MetaforceInstaller.UI/Program.cs b/MetaforceInstaller.UI/Program.cs
new file mode 100644
index 0000000..c77ddc1
--- /dev/null
+++ b/MetaforceInstaller.UI/Program.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace MetaforceInstaller.UI;
+
+class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/MetaforceInstaller.UI/app.manifest b/MetaforceInstaller.UI/app.manifest
new file mode 100644
index 0000000..0543ed4
--- /dev/null
+++ b/MetaforceInstaller.UI/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MetaforceInstaller.sln b/MetaforceInstaller.sln
index 44d9b89..b2da3d6 100644
--- a/MetaforceInstaller.sln
+++ b/MetaforceInstaller.sln
@@ -2,6 +2,10 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaforceInstaller.Cli", "MetaforceInstaller.Cli\MetaforceInstaller.Cli.csproj", "{4928C2AC-6B63-4B18-9472-705807A15893}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaforceInstaller.Core", "MetaforceInstaller.Core\MetaforceInstaller.Core.csproj", "{83961B6E-21E7-4B7A-B4FA-6128A44BCE1F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaforceInstaller.UI", "MetaforceInstaller.UI\MetaforceInstaller.UI.csproj", "{546DDE53-4607-4852-B951-434061C46FB2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +16,13 @@ Global
{4928C2AC-6B63-4B18-9472-705807A15893}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4928C2AC-6B63-4B18-9472-705807A15893}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4928C2AC-6B63-4B18-9472-705807A15893}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83961B6E-21E7-4B7A-B4FA-6128A44BCE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83961B6E-21E7-4B7A-B4FA-6128A44BCE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83961B6E-21E7-4B7A-B4FA-6128A44BCE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83961B6E-21E7-4B7A-B4FA-6128A44BCE1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {546DDE53-4607-4852-B951-434061C46FB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {546DDE53-4607-4852-B951-434061C46FB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {546DDE53-4607-4852-B951-434061C46FB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {546DDE53-4607-4852-B951-434061C46FB2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..50a5c44
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# MetaforceInstaller
+
+Написал программку MetaforceInstaller для установки новых версий с контентом в отдельном файле, вместо перетаскивания зипки и установки апк из шлема просто нажать две кнопки
+
+Инструкция:
+1. Открываете консольку (Win + R, вписать powershell, нажать Enter)
+2. Переходите в директорию с MetaforceInstaller
+3. Пишете команду
+```
+MetaforceInstaller.exe -a <путь_к_apk> -c <путь_к_zip>
+```
+4. Жмете Enter
+5. Ждете пару минут и радуетесь!!
+
+Также работает и для планшетной админки, просто укажите в аргументе -a путь к файлу с админкой
\ No newline at end of file