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/Defaults.cs b/MetaforceInstaller.Core/Defaults.cs new file mode 100644 index 0000000..ec22aad --- /dev/null +++ b/MetaforceInstaller.Core/Defaults.cs @@ -0,0 +1,7 @@ +namespace MetaforceInstaller.Core; + +public static class Defaults +{ + public static readonly string StoragePath = + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + "MetaforceInstaller"; +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Intefaces/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/Intefaces/IStorageService.cs b/MetaforceInstaller.Core/Intefaces/IStorageService.cs new file mode 100644 index 0000000..5967309 --- /dev/null +++ b/MetaforceInstaller.Core/Intefaces/IStorageService.cs @@ -0,0 +1,9 @@ +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Intefaces; + +public interface IStorageService +{ + AppData Load(); + void Save(AppData data); +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj new file mode 100644 index 0000000..0b0eeda --- /dev/null +++ b/MetaforceInstaller.Core/MetaforceInstaller.Core.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + + + + + PreserveNewest + AdbWinApi.dll + + + PreserveNewest + AdbWinUsbApi.dll + + + + + + + + + + + + + + + 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/AppData.cs b/MetaforceInstaller.Core/Models/AppData.cs new file mode 100644 index 0000000..5c37871 --- /dev/null +++ b/MetaforceInstaller.Core/Models/AppData.cs @@ -0,0 +1,6 @@ +namespace MetaforceInstaller.Core.Models; + +public class AppData +{ + public List Installations { get; set; } = new(); +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/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/InstallationData.cs b/MetaforceInstaller.Core/Models/InstallationData.cs new file mode 100644 index 0000000..6f53baa --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationData.cs @@ -0,0 +1,9 @@ +namespace MetaforceInstaller.Core.Models; + +public class InstallationData +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public required string Title { get; set; } + public required InstallationParts Parts { get; set; } + public DateTime InstalledAt { get; set; } = DateTime.Now; +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Models/InstallationParts.cs b/MetaforceInstaller.Core/Models/InstallationParts.cs new file mode 100644 index 0000000..62acb62 --- /dev/null +++ b/MetaforceInstaller.Core/Models/InstallationParts.cs @@ -0,0 +1,12 @@ +namespace MetaforceInstaller.Core.Models; + +public class InstallationParts +{ + public string? OculusClientPath { get; init; } + public string? PicoClientPath { get; init; } + public string? AndroidAdminPath { get; init; } + public string? AndroidContentPath { get; init; } + public string? WindowsContentPath { get; init; } + public string? WindowsAdminPath { get; init; } + public string? WindowsServerPath { get; init; } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/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..07ca0ed --- /dev/null +++ b/MetaforceInstaller.Core/Services/AdbService.cs @@ -0,0 +1,263 @@ +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; + private DeviceMonitor? _deviceMonitor; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + public EventHandler? DeviceConnected; + public EventHandler? DeviceDisconnected; + public EventHandler? DeviceChanged; + + public bool IsDeviceConnected => _deviceData != null && _deviceData.State == DeviceState.Online; + + public AdbService(ILogger? logger = null) + { + _logger = logger ?? new NullLogger(); + var adbPath = GetAdbPath(); + var server = new AdbServer(); + var serverStatus = server.StartServer(adbPath, restartServerIfNewer: false); + _adbClient = new AdbClient(); + RefreshDeviceData(); + StartDeviceMonitoring(); + } + + private void StartDeviceMonitoring() + { + try + { + _deviceMonitor = new DeviceMonitor(new AdbSocket(_adbClient.EndPoint)); + _deviceMonitor.DeviceConnected += OnDeviceConnected; + _deviceMonitor.DeviceDisconnected += OnDeviceDisconnected; + _deviceMonitor.DeviceChanged += OnDeviceChanged; + _deviceMonitor.Start(); + + _logger.LogInformation("Device monitoring started"); + } + catch (Exception ex) + { + _logger.LogError($"Failed to start device monitoring: {ex.Message}"); + } + } + + private void OnDeviceConnected(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device conn: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceConnected?.Invoke(this, e); + } + + private void OnDeviceDisconnected(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device disconnected: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceDisconnected?.Invoke(this, e); + } + + private void OnDeviceChanged(object? sender, DeviceDataEventArgs e) + { + _logger.LogInformation($"Device changed: {e.Device.Serial}"); + RefreshDeviceData(); + DeviceChanged?.Invoke(this, e); + } + + public void RefreshDeviceData() + { + 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); + } + + public void Dispose() + { + if (_deviceMonitor != null) + { + _deviceMonitor.DeviceConnected -= OnDeviceConnected; + _deviceMonitor.DeviceDisconnected -= OnDeviceDisconnected; + _deviceMonitor.DeviceChanged -= OnDeviceChanged; + _deviceMonitor.Dispose(); + } + } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/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/Services/StorageService.cs b/MetaforceInstaller.Core/Services/StorageService.cs new file mode 100644 index 0000000..4ab4ffa --- /dev/null +++ b/MetaforceInstaller.Core/Services/StorageService.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using MetaforceInstaller.Core.Intefaces; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public class StorageService : IStorageService +{ + private readonly string _storagePath; + + public StorageService() + { + var appDirectory = Defaults.StoragePath; + Directory.CreateDirectory(appDirectory); + _storagePath = Path.Combine(appDirectory, "installations.json"); + } + + public AppData Load() + { + if (!File.Exists(_storagePath)) + return new AppData(); + + var json = File.ReadAllText(_storagePath); + return JsonSerializer.Deserialize(json) ?? new AppData(); + } + + public void Save(AppData data) + { + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(_storagePath, json); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.Core/Services/ZipScrapper.cs b/MetaforceInstaller.Core/Services/ZipScrapper.cs new file mode 100644 index 0000000..b5c4122 --- /dev/null +++ b/MetaforceInstaller.Core/Services/ZipScrapper.cs @@ -0,0 +1,160 @@ +using System.IO.Compression; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.Core.Services; + +public class ZipScrapper +{ + public static InstallationParts PeekFiles(ZipArchive archive) + { + return new InstallationParts + { + AndroidContentPath = FindAndroidContent(archive), + OculusClientPath = FindOculusClient(archive), + PicoClientPath = FindPicoClient(archive), + AndroidAdminPath = FindAndroidAdmin(archive), + WindowsAdminPath = FindPcAdmin(archive), + WindowsContentPath = FindWindowsContent(archive), + WindowsServerPath = FindServer(archive), + }; + } + + /// + /// Extracts ZIP archive to a unique folder based on installation GUID + /// + /// ZIP archive to extract + /// Base storage path + /// Unique GUID for this installation + /// Progress reporter + /// Full path to the extracted folder + public static string ExtractZip( + ZipArchive archive, + string baseOutputPath, + Guid installationGuid, + IProgress? progress = null) + { + // Create unique folder for this installation + var installationFolder = Path.Combine(baseOutputPath, installationGuid.ToString()); + Directory.CreateDirectory(installationFolder); + + var entries = archive.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToList(); + var totalEntries = entries.Count; + var processedEntries = 0; + + foreach (var entry in entries) + { + var destinationPath = Path.Combine(installationFolder, entry.FullName); + var destinationDir = Path.GetDirectoryName(destinationPath); + + if (!string.IsNullOrEmpty(destinationDir)) + { + Directory.CreateDirectory(destinationDir); + } + + entry.ExtractToFile(destinationPath, overwrite: true); + + processedEntries++; + progress?.Report((double)processedEntries / totalEntries * 100); + } + + return installationFolder; + } + + /// + /// Updates InstallationParts paths to reflect the actual extracted location + /// + public static InstallationParts UpdatePathsAfterExtraction( + InstallationParts parts, + string extractedFolderPath) + { + return new InstallationParts + { + AndroidContentPath = UpdatePath(parts.AndroidContentPath, extractedFolderPath), + OculusClientPath = UpdatePath(parts.OculusClientPath, extractedFolderPath), + PicoClientPath = UpdatePath(parts.PicoClientPath, extractedFolderPath), + AndroidAdminPath = UpdatePath(parts.AndroidAdminPath, extractedFolderPath), + WindowsAdminPath = UpdatePath(parts.WindowsAdminPath, extractedFolderPath), + WindowsContentPath = UpdatePath(parts.WindowsContentPath, extractedFolderPath), + WindowsServerPath = UpdatePath(parts.WindowsServerPath, extractedFolderPath), + }; + } + + private static string? UpdatePath(string? relativePath, string basePath) + { + if (string.IsNullOrEmpty(relativePath)) + return null; + + return Path.Combine(basePath, relativePath); + } + + private static string? FindPicoClient(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforcePico", + extension: ".apk"); + } + + private static string? FindOculusClient(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforceOculus", + extension: ".apk"); + } + + private static string? FindAndroidAdmin(ZipArchive archive) + { + return FindEntry(archive, + name: "MetaforceAdmin", + extension: ".apk"); + } + + private static string? FindAndroidContent(ZipArchive archive) + { + return FindEntry(archive, + name: "Content_Android", + extension: ".zip"); + } + + private static string? FindWindowsContent(ZipArchive archive) + { + return FindEntry(archive, + name: "Content_StandaloneWindows", + extension: ".zip"); + } + + private static string? FindPcAdmin(ZipArchive archive) + { + return FindExecutable(archive, "MetaforceAdminPC"); + } + + private static string? FindServer(ZipArchive archive) + { + return FindExecutable(archive, "MetaforceServer"); + } + + /// + /// Finds an entry in archive by name and extension + /// + private static string? FindEntry(ZipArchive archive, string name, string extension) + { + var entry = archive.Entries.FirstOrDefault(e => + e.Name.Contains(name, StringComparison.OrdinalIgnoreCase) && + e.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + + return entry?.FullName; + } + + /// + /// Finds an executable in archive, excluding crash handlers + /// + private static string? FindExecutable(ZipArchive archive, string containsName) + { + var entry = archive.Entries.FirstOrDefault(e => + e.FullName.Contains(containsName, StringComparison.OrdinalIgnoreCase) && + e.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) && + !e.Name.Contains("UnityCrashHandler", StringComparison.OrdinalIgnoreCase) && + !e.Name.Contains("crashpad_handler", StringComparison.OrdinalIgnoreCase)); + + return entry?.FullName; + } +} \ No newline at end of file diff --git a/MetaforceInstaller.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..38eec17 --- /dev/null +++ b/MetaforceInstaller.UI/App.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using MetaforceInstaller.UI.Windows; + +namespace MetaforceInstaller.UI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + Lang.Resources.Culture = System.Globalization.CultureInfo.CurrentCulture; + 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/Lang/Resources.Designer.cs b/MetaforceInstaller.UI/Lang/Resources.Designer.cs new file mode 100644 index 0000000..a61e233 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.Designer.cs @@ -0,0 +1,251 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MetaforceInstaller.UI.Lang { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MetaforceInstaller.UI.Lang.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add new installation. + /// + public static string AddInstallation { + get { + return ResourceManager.GetString("AddInstallation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string CancelButton { + get { + return ResourceManager.GetString("CancelButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose .zip. + /// + public static string ChooseZip { + get { + return ResourceManager.GetString("ChooseZip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connected to. + /// + public static string ConnectedTo { + get { + return ResourceManager.GetString("ConnectedTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Delete { + get { + return ResourceManager.GetString("Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install admin. + /// + public static string InstallAdminCheckbox { + get { + return ResourceManager.GetString("InstallAdminCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install Android admin. + /// + public static string InstallAndroidAdmin { + get { + return ResourceManager.GetString("InstallAndroidAdmin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install. + /// + public static string InstallButton { + get { + return ResourceManager.GetString("InstallButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install server. + /// + public static string InstallServerCheckbox { + get { + return ResourceManager.GetString("InstallServerCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install VR client. + /// + public static string InstallVRClient { + get { + return ResourceManager.GetString("InstallVRClient", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch PC admin. + /// + public static string LaunchPCAdmin { + get { + return ResourceManager.GetString("LaunchPCAdmin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch server. + /// + public static string LaunchServer { + get { + return ResourceManager.GetString("LaunchServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name of new installation. + /// + public static string NameOfNewInstallation { + get { + return ResourceManager.GetString("NameOfNewInstallation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find Android content. + /// + public static string NoAndroidContentError { + get { + return ResourceManager.GetString("NoAndroidContentError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find directory with PC admin. + /// + public static string NoPCAdminError { + get { + return ResourceManager.GetString("NoPCAdminError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find directory with server. + /// + public static string NoServerError { + get { + return ResourceManager.GetString("NoServerError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not connected. + /// + public static string NotConnected { + get { + return ResourceManager.GetString("NotConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find any VR clients. + /// + public static string NoVRClientsError { + get { + return ResourceManager.GetString("NoVRClientsError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find Windows content. + /// + public static string NoWindowsContentError { + get { + return ResourceManager.GetString("NoWindowsContentError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save Android admin. + /// + public static string SaveAndroidAdminCheckbox { + get { + return ResourceManager.GetString("SaveAndroidAdminCheckbox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save VR client. + /// + public static string SaveVRClientCheckbox { + get { + return ResourceManager.GetString("SaveVRClientCheckbox", resourceCulture); + } + } + } +} diff --git a/MetaforceInstaller.UI/Lang/Resources.resx b/MetaforceInstaller.UI/Lang/Resources.resx new file mode 100644 index 0000000..c641fa3 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.resx @@ -0,0 +1,84 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Launch server + + + Launch PC admin + + + Install VR client + + + Install Android admin + + + Delete + + + Add new installation + + + Not connected + + + Connected to + + + Name of new installation + + + Choose .zip + + + Install server + + + Install admin + + + Save Android admin + + + Save VR client + + + Install + + + Cancel + + + Couldn't find directory with server + + + Couldn't find directory with PC admin + + + Couldn't find Windows content + + + Couldn't find Android content + + + Couldn't find any VR clients + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/Lang/Resources.ru.resx b/MetaforceInstaller.UI/Lang/Resources.ru.resx new file mode 100644 index 0000000..891e8d9 --- /dev/null +++ b/MetaforceInstaller.UI/Lang/Resources.ru.resx @@ -0,0 +1,77 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Запустить сервер + + + Запустить админку + + + Удалить + + + Установить андроид-админку + + + Установить на шлем + + + Добавить версию + + + Подключено: + + + Не подключено + + + Выбрать .zip + + + Отмена + + + Установить админку + + + Установить + + + Установить сервер + + + Название версии + + + Сохранить андроид-админку + + + Сохранить VR-клиенты + + + Не удалось обнаружить Андроид контент + + + Не удалось обнаружить ПК-админку + + + Не удалось обнаружить сервер + + + Не удалось обнаружить VR-клиенты + + + Не удалось обнаружить Windows контент + + \ No newline at end of file diff --git a/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj new file mode 100644 index 0000000..3a83cb5 --- /dev/null +++ b/MetaforceInstaller.UI/MetaforceInstaller.UI.csproj @@ -0,0 +1,52 @@ + + + WinExe + net8.0 + enable + true + true + true + app.manifest + true + 2.0.0-b1 + + + + + + + + + + None + All + + + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + diff --git a/MetaforceInstaller.UI/Program.cs b/MetaforceInstaller.UI/Program.cs new file mode 100644 index 0000000..c77ddc1 --- /dev/null +++ b/MetaforceInstaller.UI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace MetaforceInstaller.UI; + +class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..22c0005 --- /dev/null +++ b/MetaforceInstaller.UI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using MetaforceInstaller.Core.Models; + +namespace MetaforceInstaller.UI.ViewModels; + +public class MainWindowViewModel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + public ObservableCollection Installations { get; set; } = new(); + + private bool _isDeviceConnected; + + public bool IsDeviceConnected + { + get => _isDeviceConnected; + set + { + if (_isDeviceConnected != value) + { + _isDeviceConnected = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(StatusColor)); + OnPropertyChanged(nameof(StatusText)); + } + } + } + + private string _deviceSerial = string.Empty; + + public string DeviceSerial + { + get => _deviceSerial; + set + { + if (_deviceSerial != value) + { + _deviceSerial = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(StatusText)); + } + } + } + + public IBrush StatusColor => IsDeviceConnected ? Brushes.Green : Brushes.Red; + public string StatusText => IsDeviceConnected ? $"{Lang.Resources.ConnectedTo} {_deviceSerial}" : Lang.Resources.NotConnected; + + public void LoadInstallations(IEnumerable data) + { + Installations.Clear(); + foreach (var installation in data) + { + Installations.Add(installation); + } + } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/MetaforceInstaller.UI/Windows/MainWindow.axaml b/MetaforceInstaller.UI/Windows/MainWindow.axaml new file mode 100644 index 0000000..e7d4655 --- /dev/null +++ b/MetaforceInstaller.UI/Windows/MainWindow.axaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +