Merge pull request 'move logic to Core project and create UI project with UI lol' (#1) from unstable into master

Reviewed-on: #1
This commit is contained in:
Вячеслав 2025-09-15 03:47:39 +05:00
commit fff5432e2a
23 changed files with 832 additions and 190 deletions

View file

@ -7,14 +7,16 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" /> <PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="adb\adb.exe" /> <ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
<EmbeddedResource Include="adb\AdbWinApi.dll" />
<EmbeddedResource Include="adb\AdbWinUsbApi.dll" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,57 +1,54 @@
using System.Reflection; using MetaforceInstaller.Cli.Utils;
using AdvancedSharpAdbClient; using MetaforceInstaller.Core.Services;
using AdvancedSharpAdbClient.DeviceCommands;
using AdvancedSharpAdbClient.Models;
namespace MetaforceInstaller.Cli; namespace MetaforceInstaller.Cli;
class Program static class Program
{ {
// 1. Получить имя апк и зипки, если не предоставлены - забить дефолтными значениями static async Task Main(string[] args)
// 2. Распаковать в временную директорию adb (готово)
// 3. Установить апк
// 4. Получить имя пакета
// 5. Сформировать строку пути для контента
// 6. Копировать зип по сформированному пути
static AdbClient adbClient;
static DeviceData deviceData;
static void Main(string[] args)
{ {
try 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(); ShowUsage();
return; return;
} }
var adbPath = ExtractAdbFiles(); var adbService = new AdbService();
var server = new AdbServer(); var apkInfo = ApkScrapper.GetApkInfo(installationRequest.ApkPath);
var result = server.StartServer(adbPath, restartServerIfNewer: false); var zipName = Path.GetFileName(installationRequest.ZipPath);
Console.WriteLine($"ADB сервер запущен: {result}"); 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()) // Создание объекта для отслеживания прогресса
{ var progress = new Progress<MetaforceInstaller.Core.Models.ProgressInfo>(OnProgressReport);
Console.WriteLine("Устройства не найдены. Подключите Android-устройство и включите отладку по USB.");
return;
}
deviceData = devices.FirstOrDefault(); // Установка APK
Console.WriteLine($"Найдено устройство: {deviceData.Serial}"); await adbService.InstallApkAsync(installationRequest.ApkPath, progress);
Console.WriteLine($"Состояние: {deviceData.State}"); Console.WriteLine();
Console.WriteLine($"Имя устройства: {deviceData.Name} - {deviceData.Model}");
InstallApk(apkPath); // Копирование файла
CopyFileToDevice(zipPath, outputPath); await adbService.CopyFileAsync(installationRequest.ZipPath, outputPath, progress);
Console.WriteLine();
Console.WriteLine("Операция завершена успешно!");
} }
catch (Exception ex) 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() static void ShowUsage()
{ {
Console.WriteLine("Использование:"); Console.WriteLine("Использование:");
@ -78,143 +98,6 @@ class Program
Console.WriteLine(" MetaforceInstaller.exe -a app.apk -c data.zip -o /sdcard/data.zip"); 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)
{
string apkPath = null;
string zipPath = null;
string outputPath = null;
for (int i = 0; i < args.Length; i++)
{
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;
}
}
return (apkPath, zipPath, outputPath);
}
private static void ExtractResource(string resourceName, string outputPath)
{
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
using var fileStream = File.Create(outputPath);
stream.CopyTo(fileStream);
}
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))
{
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"));
}
return adbPath;
}
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<InstallProgressEventArgs>(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<SyncProgressChangedEventArgs>(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) private static void DrawProgressBar(int progress, long receivedBytes, long totalBytes)
{ {
Console.SetCursorPosition(0, Console.CursorTop); Console.SetCursorPosition(0, Console.CursorTop);
@ -223,14 +106,19 @@ private static void DrawProgressBar(int progress, long receivedBytes, long total
var filledLength = (int)(barLength * progress / 100.0); var filledLength = (int)(barLength * progress / 100.0);
var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]"; var bar = "[" + new string('█', filledLength) + new string('░', barLength - filledLength) + "]";
var bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}";
string bytesText = "";
if (totalBytes > 0)
{
bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}";
}
Console.Write($"\r{bar} {progress}%{bytesText}"); Console.Write($"\r{bar} {progress}%{bytesText}");
} }
private static string FormatBytes(long bytes) private static string FormatBytes(long bytes)
{ {
string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
var counter = 0; var counter = 0;
double number = bytes; double number = bytes;

View file

@ -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;
}
}

View file

@ -0,0 +1,18 @@
using MetaforceInstaller.Core.Models;
using MetaforceInstaller.Core.Models;
namespace MetaforceInstaller.Core.Intefaces;
public interface IAdbService
{
event EventHandler<ProgressInfo>? ProgressChanged;
event EventHandler<string>? StatusChanged;
Task InstallApkAsync(string apkPath, IProgress<ProgressInfo>? progress = null, CancellationToken cancellationToken = default);
Task CopyFileAsync(string localPath, string remotePath, IProgress<ProgressInfo>? progress = null, CancellationToken cancellationToken = default);
DeviceInfo GetDeviceInfo();
// Синхронные версии для обратной совместимости
void InstallApk(string apkPath);
void CopyFile(string localPath, string remotePath);
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="adb\adb.exe" />
<EmbeddedResource Include="adb\AdbWinApi.dll" />
<EmbeddedResource Include="adb\AdbWinUsbApi.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlphaOmega.ApkReader" Version="2.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
namespace MetaforceInstaller.Core.Models;
public record ApkInfo(string PackageName, string VersionName, string VersionCode);

View file

@ -0,0 +1,8 @@
namespace MetaforceInstaller.Core.Models;
public record DeviceInfo(
string SerialNumber,
string State,
string Model,
string Name
);

View file

@ -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; }
}

View file

@ -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
}

View file

@ -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<AdbService> _logger;
private readonly AdbClient _adbClient;
private DeviceData _deviceData;
public event EventHandler<ProgressInfo>? ProgressChanged;
public event EventHandler<string>? StatusChanged;
public AdbService(ILogger<AdbService>? logger = null)
{
_logger = logger ?? new NullLogger<AdbService>();
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<ProgressInfo>? 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<ProgressInfo>? 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);
}
}

View file

@ -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");
}
}

View file

@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MetaforceInstaller.UI.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View file

@ -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();
}
}

View file

@ -0,0 +1,49 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MetaforceInstaller.UI.MainWindow"
Title="MetaforceInstaller">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Main Content Area -->
<Grid Grid.Row="0" Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/> <!-- 30% -->
<ColumnDefinition Width="7*"/> <!-- 70% -->
</Grid.ColumnDefinitions>
<!-- Left Panel - Buttons -->
<StackPanel Grid.Column="0" Margin="20" Spacing="15">
<Button Name="ChooseApkButton" Content="Choose .apk"
HorizontalAlignment="Stretch"/>
<Button Name="ChooseContentButton" Content="Choose .zip"
HorizontalAlignment="Stretch"/>
<Button Name="InstallButton" Content="Install"
HorizontalAlignment="Stretch"/>
</StackPanel>
<!-- Right Panel - Logs -->
<TextBox Grid.Column="1" Name="LogsTextBox"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
FontFamily="Consolas,Courier New,monospace"
Focusable="False"
Margin="4,0,0,0"/>
</Grid>
<!-- Progress Bar at Bottom -->
<ProgressBar Grid.Row="1" Name="InstallProgressBar"
Height="20" Minimum="0" Maximum="100" Value="0"
Margin="8"/>
</Grid>
</Window>

View file

@ -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<ProgressInfo>(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>();
scrollViewer?.ScrollToEnd();
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.6"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.6"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="MetaforceInstaller.UI.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View file

@ -2,6 +2,10 @@
Microsoft Visual Studio Solution File, Format Version 12.00 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaforceInstaller.Cli", "MetaforceInstaller.Cli\MetaforceInstaller.Cli.csproj", "{4928C2AC-6B63-4B18-9472-705807A15893}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{4928C2AC-6B63-4B18-9472-705807A15893}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# MetaforceInstaller
Написал программку MetaforceInstaller для установки новых версий с контентом в отдельном файле, вместо перетаскивания зипки и установки апк из шлема просто нажать две кнопки
Инструкция:
1. Открываете консольку (Win + R, вписать powershell, нажать Enter)
2. Переходите в директорию с MetaforceInstaller
3. Пишете команду
```
MetaforceInstaller.exe -a <путь_к_apk> -c <путь_к_zip>
```
4. Жмете Enter
5. Ждете пару минут и радуетесь!!
Также работает и для планшетной админки, просто укажите в аргументе -a путь к файлу с админкой