diff --git a/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj new file mode 100644 index 0000000..a267acc --- /dev/null +++ b/MetaforceInstaller.Cli/MetaforceInstaller.Cli.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + true + true + enable + + + + + + + + + + + + + + + diff --git a/MetaforceInstaller.Cli/Program.cs b/MetaforceInstaller.Cli/Program.cs new file mode 100644 index 0000000..bf8bf51 --- /dev/null +++ b/MetaforceInstaller.Cli/Program.cs @@ -0,0 +1,133 @@ +using MetaforceInstaller.Cli.Utils; +using MetaforceInstaller.Core.Services; + +namespace MetaforceInstaller.Cli; + +static class Program +{ + static async Task Main(string[] args) + { + try + { + var installationRequest = ArgumentParser.ParseArguments(args); + + if (installationRequest is null || + string.IsNullOrEmpty(installationRequest.ApkPath) || + string.IsNullOrEmpty(installationRequest.ZipPath)) + { + ShowUsage(); + return; + } + + var adbService = new AdbService(); + + var apkInfo = ApkScrapper.GetApkInfo(installationRequest.ApkPath); + var zipName = Path.GetFileName(installationRequest.ZipPath); + var outputPath = + @$"/storage/emulated/0/Android/data/{apkInfo.PackageName}/files/{zipName}"; + + // Подписка на события прогресса + adbService.ProgressChanged += OnProgressChanged; + adbService.StatusChanged += OnStatusChanged; + + // Получение информации об устройстве + var deviceInfo = adbService.GetDeviceInfo(); + Console.WriteLine($"Найдено устройство: {deviceInfo.SerialNumber}"); + Console.WriteLine($"Состояние: {deviceInfo.State}"); + Console.WriteLine($"Модель: {deviceInfo.Model} - {deviceInfo.Name}"); + Console.WriteLine(); + + // Создание объекта для отслеживания прогресса + var progress = new Progress(OnProgressReport); + + // Установка APK + await adbService.InstallApkAsync(installationRequest.ApkPath, progress); + Console.WriteLine(); + + // Копирование файла + await adbService.CopyFileAsync(installationRequest.ZipPath, outputPath, progress); + Console.WriteLine(); + + Console.WriteLine("Операция завершена успешно!"); + } + catch (Exception ex) + { + Console.WriteLine($"Ошибка: {ex.Message}"); + } + } + + 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("Использование:"); + Console.WriteLine( + " MetaforceInstaller.exe --apk <путь_к_apk> --content <путь_к_zip> --output <путь_для контента>"); + Console.WriteLine(" MetaforceInstaller.exe -a <путь_к_apk> -c <путь_к_zip> -o <путь_для_контента>"); + Console.WriteLine(); + Console.WriteLine("Параметры:"); + Console.WriteLine(" --apk, -a Путь к APK файлу"); + Console.WriteLine(" --content, -c Путь к ZIP файлу с контентом"); + Console.WriteLine(" --output, -o Путь для копирования контента"); + Console.WriteLine(" --help, -h Показать эту справку"); + Console.WriteLine(); + Console.WriteLine("Пример:"); + Console.WriteLine( + " MetaforceInstaller.exe --apk \"C:\\app.apk\" --content \"C:\\data.zip\" --output \"/sdcard/data.zip\""); + Console.WriteLine(" MetaforceInstaller.exe -a app.apk -c data.zip -o /sdcard/data.zip"); + } + + 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) + "]"; + + 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"]; + 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.Cloud/ICloudService.cs b/MetaforceInstaller.Cloud/ICloudService.cs deleted file mode 100644 index a179bba..0000000 --- a/MetaforceInstaller.Cloud/ICloudService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MetaforceInstaller.Cloud.Models; - -namespace MetaforceInstaller.Cloud; - -public interface ICloudService -{ - Task> GetObjects(string webLink); -} \ No newline at end of file diff --git a/MetaforceInstaller.Cloud/Implementations/MailRu/DTO/CloudObjectResponse.cs b/MetaforceInstaller.Cloud/Implementations/MailRu/DTO/CloudObjectResponse.cs deleted file mode 100644 index 7f2bdb3..0000000 --- a/MetaforceInstaller.Cloud/Implementations/MailRu/DTO/CloudObjectResponse.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MetaforceInstaller.Cloud.Implementations.MailRu.DTO; - -public sealed class CloudObjectsResponse -{ - [JsonPropertyName("count")] - public CountInfo? Count { get; init; } - - [JsonPropertyName("name")] - public string? Name { get; init; } - - [JsonPropertyName("weblink")] - public string? WebLink { get; init; } - - [JsonPropertyName("size")] - public long? Size { get; init; } - - [JsonPropertyName("rev")] - public long? Rev { get; init; } - - [JsonPropertyName("kind")] - public string? Kind { get; init; } - - [JsonPropertyName("type")] - public string? Type { get; init; } - - [JsonPropertyName("public")] - public PublicInfo? Public { get; init; } - - [JsonPropertyName("list")] - public List? List { get; init; } - - [JsonPropertyName("owner")] - public OwnerInfo? Owner { get; init; } -} - -public sealed class CountInfo -{ - [JsonPropertyName("folders")] - public int? Folders { get; init; } - - [JsonPropertyName("files")] - public int? Files { get; init; } -} - -public sealed class PublicInfo -{ - [JsonPropertyName("type")] - public string? Type { get; init; } - - [JsonPropertyName("name")] - public string? Name { get; init; } - - [JsonPropertyName("id")] - public string? Id { get; init; } - - [JsonPropertyName("ctime")] - public long? CTime { get; init; } - - [JsonPropertyName("views")] - public long? Views { get; init; } - - [JsonPropertyName("downloads")] - public long? Downloads { get; init; } -} - -public sealed class OwnerInfo -{ - [JsonPropertyName("email")] - public string? Email { get; init; } - - [JsonPropertyName("user_flags")] - public UserFlags? UserFlags { get; init; } -} - -public sealed class UserFlags -{ - [JsonPropertyName("PAID_ACCOUNT")] - public bool? PaidAccount { get; init; } -} - -/// -/// Базовый тип элемента из "list". Конкретный тип выбирается по полю "type". -/// -[JsonConverter(typeof(CloudObjectJsonConverter))] -public abstract class CloudObjectBase -{ - [JsonPropertyName("name")] - public string? Name { get; init; } - - [JsonPropertyName("weblink")] - public string? WebLink { get; init; } - - [JsonPropertyName("size")] - public long? Size { get; init; } - - [JsonPropertyName("kind")] - public string? Kind { get; init; } - - [JsonPropertyName("type")] - public string? Type { get; init; } -} - -public sealed class CloudFolderObject : CloudObjectBase -{ - [JsonPropertyName("count")] - public CountInfo? Count { get; init; } - - [JsonPropertyName("rev")] - public long? Rev { get; init; } -} - -public sealed class CloudFileObject : CloudObjectBase -{ - [JsonPropertyName("mtime")] - public long? MTime { get; init; } - - [JsonPropertyName("hash")] - public string? Hash { get; init; } -} - -public sealed class ZipWebLinkRequest -{ - [JsonPropertyName("x-email")] - public string XEmail { get; init; } - - [JsonPropertyName("weblink_list")] - public IReadOnlyList WeblinkList { get; init; } - - [JsonPropertyName("name")] - public string Name { get; init; } -} - -public sealed class ZipWebLinkResponse -{ - [JsonPropertyName("key")] - public string Key { get; init; } -} - -/// -/// Конвертер, который смотрит на поле "type" и десериализует в CloudFolderObject или CloudFileObject. -/// -public sealed class CloudObjectJsonConverter : JsonConverter -{ - public override CloudObjectBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var doc = JsonDocument.ParseValue(ref reader); - - var root = doc.RootElement; - if (!root.TryGetProperty("type", out var typeProp)) - throw new JsonException("Missing required property 'type' for cloud object."); - - var type = typeProp.GetString(); - - return type switch - { - "folder" => root.Deserialize(options) - ?? throw new JsonException("Failed to deserialize folder object."), - "file" => root.Deserialize(options) - ?? throw new JsonException("Failed to deserialize file object."), - _ => throw new JsonException($"Unknown cloud object type '{type}'.") - }; - } - - public override void Write(Utf8JsonWriter writer, CloudObjectBase value, JsonSerializerOptions options) - { - switch (value) - { - case CloudFolderObject folder: - JsonSerializer.Serialize(writer, folder, options); - break; - case CloudFileObject file: - JsonSerializer.Serialize(writer, file, options); - break; - default: - throw new JsonException($"Unknown runtime type '{value.GetType().Name}'."); - } - } -} \ No newline at end of file diff --git a/MetaforceInstaller.Cloud/Implementations/MailRu/MailRuCloudService.cs b/MetaforceInstaller.Cloud/Implementations/MailRu/MailRuCloudService.cs deleted file mode 100644 index d7ae0ca..0000000 --- a/MetaforceInstaller.Cloud/Implementations/MailRu/MailRuCloudService.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Web; -using MetaforceInstaller.Cloud.Implementations.MailRu.DTO; -using MetaforceInstaller.Cloud.Models; - -namespace MetaforceInstaller.Cloud.Implementations.MailRu; - -public class MailRuCloudService : ICloudService -{ - private const string BaseUrl = "https://cloud.mail.ru"; - - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true - }; - - private readonly HttpClient _httpClient; - - public MailRuCloudService(HttpClient httpClient) - { - _httpClient = httpClient; - } - - public async Task> GetObjects(string webLink) - { - var dto = await GetFromPublicApi( - path: "/api/v4/public/list", - query: new Dictionary - { - ["weblink"] = webLink, - ["sort"] = "name", - ["order"] = "asc", - ["offset"] = "0", - ["limit"] = "500", - ["version"] = "4" - }).ConfigureAwait(false); - - if (dto?.List is null || dto.List.Count == 0) - return []; - - var result = new List(dto.List.Count); - foreach (var entry in dto.List) - result.Add(Map(entry)); - - return result; - } - - public async Task GetDownloadLink(IEnumerable webLinks, string outputFileName = "archive") - { - var result = await PostToPublicApi( - path: "/api/v3/zip/weblink", - query: new Dictionary(), - new ZipWebLinkRequest - { - XEmail = "anonym", - WeblinkList = webLinks.ToList(), - Name = outputFileName - } - ); - - return result?.Key; - } - - private async Task GetFromPublicApi(string path, IReadOnlyDictionary query) - { - var uri = BuildUri(path, query); - - using var response = await _httpClient.GetAsync(uri).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - return JsonSerializer.Deserialize(json, JsonOptions); - } - - private async Task PostToPublicApi(string path, IReadOnlyDictionary query, - object body) - { - var uri = BuildUri(path, query); - - var jsonBody = JsonSerializer.Serialize(body, JsonOptions); - using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - - using var response = await _httpClient.PostAsync(uri, content).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - return JsonSerializer.Deserialize(json, JsonOptions); - } - - private static Uri BuildUri(string path, IReadOnlyDictionary query) - { - var builder = new UriBuilder(BaseUrl) - { - Port = -1, - Path = path - }; - - var qs = HttpUtility.ParseQueryString(builder.Query); - foreach (var (key, value) in query) - qs[key] = value; - - builder.Query = qs.ToString(); - return builder.Uri; - } - - private static CloudObject Map(CloudObjectBase entry) - { - return new CloudObject - { - Name = entry.Name, - WebLink = entry.WebLink, - Type = entry.Type == "folder" ? CloudObjectType.Folder : CloudObjectType.File - }; - } -} \ No newline at end of file diff --git a/MetaforceInstaller.Cloud/MetaforceInstaller.Cloud.csproj b/MetaforceInstaller.Cloud/MetaforceInstaller.Cloud.csproj deleted file mode 100644 index 17b910f..0000000 --- a/MetaforceInstaller.Cloud/MetaforceInstaller.Cloud.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net9.0 - enable - enable - - - diff --git a/MetaforceInstaller.Cloud/Models/CloudObject.cs b/MetaforceInstaller.Cloud/Models/CloudObject.cs deleted file mode 100644 index 643a9d5..0000000 --- a/MetaforceInstaller.Cloud/Models/CloudObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MetaforceInstaller.Cloud.Models; - -public class CloudObject -{ - public string Name { get; set; } - public string WebLink { get; set; } - public CloudObjectType Type { get; set; } -} - -public enum CloudObjectType -{ - Folder, - File -} \ 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..ddf790f --- /dev/null +++ b/MetaforceInstaller.Core/Services/AdbService.cs @@ -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 _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 + }); + + 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); + } +} \ 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.Core/adb/AdbWinApi.dll b/MetaforceInstaller.Core/adb/AdbWinApi.dll new file mode 100644 index 0000000..7abe26c Binary files /dev/null and b/MetaforceInstaller.Core/adb/AdbWinApi.dll differ diff --git a/MetaforceInstaller.Core/adb/AdbWinUsbApi.dll b/MetaforceInstaller.Core/adb/AdbWinUsbApi.dll new file mode 100644 index 0000000..e7a6de1 Binary files /dev/null and b/MetaforceInstaller.Core/adb/AdbWinUsbApi.dll differ diff --git a/MetaforceInstaller.Core/adb/adb.exe b/MetaforceInstaller.Core/adb/adb.exe new file mode 100644 index 0000000..85bfeaa Binary files /dev/null and b/MetaforceInstaller.Core/adb/adb.exe differ 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/Images/logo_black.svg b/MetaforceInstaller.UI/Images/logo_black.svg new file mode 100644 index 0000000..d41fcc9 --- /dev/null +++ b/MetaforceInstaller.UI/Images/logo_black.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/Images/logo_red.svg b/MetaforceInstaller.UI/Images/logo_red.svg new file mode 100644 index 0000000..1b73fdc --- /dev/null +++ b/MetaforceInstaller.UI/Images/logo_red.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/MetaforceInstaller.UI/Images/logo_white.svg b/MetaforceInstaller.UI/Images/logo_white.svg new file mode 100644 index 0000000..12ba8d1 --- /dev/null +++ b/MetaforceInstaller.UI/Images/logo_white.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/MainWindow.axaml b/MetaforceInstaller.UI/MainWindow.axaml new file mode 100644 index 0000000..61a426d --- /dev/null +++ b/MetaforceInstaller.UI/MainWindow.axaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +