Compare commits

..

19 commits
v1.0 ... master

Author SHA1 Message Date
520bcb7965 Merge pull request 'fix secure_mkdirs' (#3) from unstable into master
Reviewed-on: #3
2025-09-24 15:36:32 +05:00
b4ce64456e Merge branch 'master' into unstable 2025-09-24 15:36:25 +05:00
6d1833d7ab pump version to 1.2.1 2025-09-24 15:29:17 +05:00
5864beffb2 actualize version 2025-09-23 21:41:16 +05:00
4f4f33b7f7 maybe solve "secure_mkdirs" error 2025-09-23 21:35:22 +05:00
64a4479b73 Merge pull request 'add logo' (#2) from unstable into master
Reviewed-on: #2
2025-09-16 17:47:26 +05:00
f026fd95a4 Merge branch 'master' into unstable 2025-09-16 17:47:18 +05:00
eceffd7ddc change logo to red 2025-09-16 16:45:23 +05:00
6a15485734 edit cool message on start due to copyright issues 2025-09-16 15:10:09 +05:00
522d410a08 added logo with color 2025-09-16 15:03:47 +05:00
d865c2f051 single file 2025-09-15 03:49:08 +05:00
fff5432e2a Merge pull request 'move logic to Core project and create UI project with UI lol' (#1) from unstable into master
Reviewed-on: #1
2025-09-15 03:47:39 +05:00
f424b99d16 add README.md 2025-09-15 03:34:18 +05:00
bbf50fbf37 theoretically i just added support for multiple installations in a row 2025-09-15 03:30:16 +05:00
69d932f6c8 it's working and we don't question it 2025-09-15 03:11:14 +05:00
1e8f8d6dc9 oh my gotto no mor outputto 2025-09-15 02:39:57 +05:00
4d5371be5e dummy ui 2025-09-15 02:08:19 +05:00
243224a720 now with animations lol 2025-09-15 00:38:30 +05:00
7a7cefe7b5 WIP: move logic to Core project 2025-09-15 00:12:36 +05:00
26 changed files with 948 additions and 190 deletions

View file

@ -4,17 +4,21 @@
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="adb\adb.exe" />
<EmbeddedResource Include="adb\AdbWinApi.dll" />
<EmbeddedResource Include="adb\AdbWinUsbApi.dll" />
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -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<MetaforceInstaller.Core.Models.ProgressInfo>(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,159 +98,27 @@ 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;
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)
{
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)}";
string bytesText = "";
if (totalBytes > 0)
{
bytesText = $" {FormatBytes(receivedBytes)} / {FormatBytes(totalBytes)}";
}
Console.Write($"\r{bar} {progress}%{bytesText}");
}
}
private static string FormatBytes(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
private static string FormatBytes(long bytes)
{
string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
var counter = 0;
double number = bytes;
@ -241,5 +129,5 @@ private static string FormatBytes(long bytes)
}
return $"{number:N1} {suffixes[counter]}";
}
}
}

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,206 @@
using System.Reflection;
using AdvancedSharpAdbClient;
using AdvancedSharpAdbClient.DeviceCommands;
using AdvancedSharpAdbClient.Models;
using AdvancedSharpAdbClient.Receivers;
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
});
var remoteDir = Path.GetDirectoryName(remotePath)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(remoteDir))
{
var reciever = new ConsoleOutputReceiver();
await Task.Run(
() => { _adbClient.ExecuteRemoteCommand($"mkdir -p \"{remoteDir}\"", _deviceData, reciever); },
cancellationToken);
}
_logger.LogInformation($"Ensured remote directory: {remoteDir}");
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,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 678.69 65.86">
<g id="_Слой_1-2" data-name="Слой 1">
<g>
<polygon points="43.16 44.66 42.89 44.66 20.1 1.24 0 1.24 0 64.58 13.87 64.58 13.87 16.65 14.22 16.65 38.65 64.58 47.49 64.58 71.83 16.65 72.31 16.65 72.31 64.58 86.05 64.58 86.05 1.24 65.95 1.24 43.16 44.66"/>
<polygon points="97.93 64.58 159.2 64.58 159.2 51.99 111.8 51.99 111.8 39.45 129.1 39.45 129.1 36.26 155.85 36.26 155.85 24.95 129.1 24.95 129.1 28.13 111.8 28.13 111.8 13.82 159.2 13.82 159.2 1.24 97.93 1.24 97.93 64.58"/>
<polygon points="232.77 1.24 168.9 1.24 168.9 13.83 193.94 13.83 193.94 64.58 207.68 64.58 207.68 13.83 232.77 13.83 232.77 1.24"/>
<polygon points="254.69 1.24 223.77 64.58 239.15 64.58 263.04 14 263.4 14 287.47 64.58 302.75 64.58 271.79 1.24 254.69 1.24"/>
<path d="m445.01,9.28c-7.36-6.18-16.83-9.28-28.4-9.28s-21.06,3.09-28.45,9.28c-7.39,6.18-11.09,14.06-11.09,23.63s3.7,17.49,11.09,23.68c7.39,6.18,16.87,9.28,28.45,9.28s21.04-3.09,28.4-9.28c7.36-6.18,11.04-14.08,11.04-23.68s-3.68-17.45-11.04-23.63Zm-9.89,38.25c-4.8,3.83-10.97,5.74-18.51,5.74s-13.75-1.91-18.55-5.74c-4.83-3.86-7.24-8.73-7.24-14.62s2.41-10.78,7.24-14.58c4.8-3.8,10.98-5.7,18.55-5.7s13.68,1.9,18.51,5.7c4.8,3.8,7.2,8.66,7.2,14.58s-2.4,10.79-7.2,14.62Z"/>
<path d="m593.41,51.9c-4.39.91-8.58,1.37-12.59,1.37-8.54,0-15.45-1.78-20.72-5.34-5.33-3.56-8-8.39-8-14.49s2.59-10.76,7.77-14.8c5.15-4,12.09-6.01,20.81-6.01,8.13.03,14.83,1.4,20.1,4.11.03-.03.6-2.09,1.72-6.18,1.09-3.98,1.63-6.05,1.63-6.23-7.16-2.88-14.84-4.33-23.06-4.33-12.99,0-23.37,3.14-31.14,9.41-7.77,6.27-11.66,14.28-11.66,24.03s3.9,17.63,11.71,23.54c7.77,5.92,18.14,8.88,31.1,8.88,9.81,0,18.13-1.72,24.96-5.17l-2.25-12.1c-2.47,1.33-5.93,2.43-10.38,3.31Z"/>
<polygon points="307.02 31 311.21 31 311.21 64.58 325.08 64.58 325.08 42.4 367.23 42.4 367.23 31 320.89 31 320.89 13.82 367.23 13.82 367.23 1.24 307.02 1.24 307.02 31"/>
<path d="m530.77,21.99h0c.05-.53.08-1.07.08-1.62,0-5.86-2.48-10.52-7.43-13.96-5.01-3.44-11.5-5.16-19.48-5.16h-42.14v20.75h3.88v42.59h13.87V21.98h-3.88v-8.06l29.82-.11c3.27,0,5.98.77,8.12,2.31,2.09,1.52,3.15,3.49,3.19,5.86v.14c0,2.47-1.06,4.5-3.19,6.09-2.14,1.62-4.86,2.43-8.12,2.43h-20.14v11.23h12.62l17.95,22.7h16.18l-19.2-24.29c18.08-3.97,17.63-15.7,17.88-18.3Z"/>
<polygon points="678.69 13.82 678.69 1.24 616.36 1.24 616.36 28.21 621.5 28.21 621.5 39.45 616.36 39.45 616.36 64.58 678.69 64.58 678.69 51.99 630.23 51.99 630.23 39.45 674.27 39.45 674.27 28.13 630.23 28.13 630.23 13.82 678.69 13.82"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 796.5 168.5" style="enable-background:new 0 0 796.5 168.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#E62027;}
</style>
<g id="Guides_For_Artboard">
</g>
<g id="Layer_1">
<rect class="st0" width="796.5" height="168.5"/>
<g>
<path class="st1" d="M478.5,42.2c-3,0-8.1,0.6-8.1,7.2v68.7c0,6.6,5.1,8,8.1,8c4.4,0,8.1-2.7,8.1-8V49.4
C486.6,44.1,482.9,42.2,478.5,42.2z"/>
<path class="st1" d="M332.6,39.4c1.2-2.2-0.4-4.8-2.9-4.8c-2.5,0-4.1,2.6-2.9,4.8l1.1,2.1l-6.1,63.1h14.6l-4.8-63.1L332.6,39.4z"
/>
<path class="st1" d="M0,0v168.5h796.5V0H0z M139.3,141.2h-14.4V76.5l-17.1,64.7H93.4L76.6,76.5v64.7H62.2V28.1h0.2h14.3h0.1
l23.9,86.5l24.1-86.5h0.1h14.3h0.2V141.2z M206.6,76.5l-4.2,13.7h-17.8v37.1h18.1l4.2,14h-36.9V28.1l36.9,0.3l-4.2,13.9h-18.1
v34.2H206.6z M278.2,42.3H264v98.9h-16.8V42.3h-14.1V28.4h45.1V42.3z M339.3,141.2l-1.6-22.8h-17.3l-2.4,22.8h-13.8l10.5-112h30.1
l10.4,112H339.3z M425.4,77.2l-4.2,13.7h-18v50.3h-14.7V28.4h36.9l-4.2,13.9h-18v34.9H425.4z M502.3,49.3v69v0.1
c0,14.9-7.8,21.8-23.6,21.8c-15.9,0-24-6.9-24-21.8v-0.1v-69v-0.1c0-14.9,8.2-21.8,24-21.8c15.9,0,23.6,6.9,23.6,21.8V49.3z
M566.8,141.2l-7.9-48.1c4.8-0.3,7.5-2.3,7.5-8.1V50.2c0-6.2-3.1-8-8.6-8h-8.1v99h-14.5V28.4h27c16.4,0,19.2,6.7,19.2,21.8V84
c0,7.5-2.2,12.9-6.7,16.4l-0.3,0.2l7.3,40.6H566.8z M660.8,75.8L645.5,71h-0.1V49.3c0-3.3-1.4-5.6-3.5-6.9
c-1.5-0.8-3.1-1.1-4.4-1.1c-2.5,0-8.2,1-8.2,8v69.1c0,2.8,0.9,4.6,2.2,5.9c1.9,1.7,4.4,2.1,5.9,2.1c3,0,8-1.4,8-8V96.4l15.3-4.5
v0.4l0.1,26.3c0,14.9-7.4,21.8-23.3,21.8c-11.4,0-18.4-3.6-21.5-11.1c-1.3-3-1.9-6.6-1.9-10.8V49.1c0-14.9,7.4-21.8,23.3-21.8
c10,0,16.7,2.8,20.2,8.6c2.1,3.4,3.2,7.8,3.2,13.3V75.8z M733.9,76.5l-4.2,13.7h-17.8v37.1h18.1l4.2,14h-36.9V28.4h36.9l-4.2,13.9
h-18.1v34.2H733.9z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 678.69 65.86">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g id="_Слой_1-2" data-name="Слой 1">
<g>
<polygon class="cls-1" points="43.16 44.66 42.89 44.66 20.1 1.24 0 1.24 0 64.58 13.87 64.58 13.87 16.65 14.22 16.65 38.65 64.58 47.49 64.58 71.83 16.65 72.31 16.65 72.31 64.58 86.05 64.58 86.05 1.24 65.95 1.24 43.16 44.66"/>
<polygon class="cls-1" points="97.93 64.58 159.2 64.58 159.2 51.99 111.8 51.99 111.8 39.45 129.1 39.45 129.1 36.26 155.85 36.26 155.85 24.95 129.1 24.95 129.1 28.13 111.8 28.13 111.8 13.82 159.2 13.82 159.2 1.24 97.93 1.24 97.93 64.58"/>
<polygon class="cls-1" points="232.77 1.24 168.9 1.24 168.9 13.83 193.94 13.83 193.94 64.58 207.68 64.58 207.68 13.83 232.77 13.83 232.77 1.24"/>
<polygon class="cls-1" points="254.69 1.24 223.77 64.58 239.15 64.58 263.04 14 263.4 14 287.47 64.58 302.75 64.58 271.79 1.24 254.69 1.24"/>
<path class="cls-1" d="m445.01,9.28c-7.36-6.18-16.83-9.28-28.4-9.28s-21.06,3.09-28.45,9.28c-7.39,6.18-11.09,14.06-11.09,23.63s3.7,17.49,11.09,23.68c7.39,6.18,16.87,9.28,28.45,9.28s21.04-3.09,28.4-9.28c7.36-6.18,11.04-14.08,11.04-23.68s-3.68-17.45-11.04-23.63Zm-9.89,38.25c-4.8,3.83-10.97,5.74-18.51,5.74s-13.75-1.91-18.55-5.74c-4.83-3.86-7.24-8.73-7.24-14.62s2.41-10.78,7.24-14.58c4.8-3.8,10.98-5.7,18.55-5.7s13.68,1.9,18.51,5.7c4.8,3.8,7.2,8.66,7.2,14.58s-2.4,10.79-7.2,14.62Z"/>
<path class="cls-1" d="m593.41,51.9c-4.39.91-8.58,1.37-12.59,1.37-8.54,0-15.45-1.78-20.72-5.34-5.33-3.56-8-8.39-8-14.49s2.59-10.76,7.77-14.8c5.15-4,12.09-6.01,20.81-6.01,8.13.03,14.83,1.4,20.1,4.11.03-.03.6-2.09,1.72-6.18,1.09-3.98,1.63-6.05,1.63-6.23-7.16-2.88-14.84-4.33-23.06-4.33-12.99,0-23.37,3.14-31.14,9.41-7.77,6.27-11.66,14.28-11.66,24.03s3.9,17.63,11.71,23.54c7.77,5.92,18.14,8.88,31.1,8.88,9.81,0,18.13-1.72,24.96-5.17l-2.25-12.1c-2.47,1.33-5.93,2.43-10.38,3.31Z"/>
<polygon class="cls-1" points="307.02 31 311.21 31 311.21 64.58 325.08 64.58 325.08 42.4 367.23 42.4 367.23 31 320.89 31 320.89 13.82 367.23 13.82 367.23 1.24 307.02 1.24 307.02 31"/>
<path class="cls-1" d="m530.77,21.99h0c.05-.53.08-1.07.08-1.62,0-5.86-2.48-10.52-7.43-13.96-5.01-3.44-11.5-5.16-19.48-5.16h-42.14v20.75h3.88v42.59h13.87V21.98h-3.88v-8.06l29.82-.11c3.27,0,5.98.77,8.12,2.31,2.09,1.52,3.15,3.49,3.19,5.86v.14c0,2.47-1.06,4.5-3.19,6.09-2.14,1.62-4.86,2.43-8.12,2.43h-20.14v11.23h12.62l17.95,22.7h16.18l-19.2-24.29c18.08-3.97,17.63-15.7,17.88-18.3Z"/>
<polygon class="cls-1" points="678.69 13.82 678.69 1.24 616.36 1.24 616.36 28.21 621.5 28.21 621.5 39.45 616.36 39.45 616.36 64.58 678.69 64.58 678.69 51.99 630.23 51.99 630.23 39.45 674.27 39.45 674.27 28.13 630.23 28.13 630.23 13.82 678.69 13.82"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,70 @@
<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">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SvgImage x:Key="Logo" Source="/Images/logo_red.svg"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SvgImage x:Key="Logo" Source="/Images/logo_red.svg"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Window.Resources>
<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>
<Image Grid.Column="0"
Margin="20"
VerticalAlignment="Bottom"
Source="{DynamicResource Logo}"
HorizontalAlignment="Center">
</Image>
<!-- 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("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<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,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<FileVersion>1.2.1</FileVersion>
</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>
<PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Images\logo_black.svg" />
<AvaloniaResource Include="Images\logo_white.svg" />
<AvaloniaResource Include="Images\logo_red.svg" />
</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
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

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 путь к файлу с админкой