Compare commits

..

2 commits

Author SHA1 Message Date
2c3f19d7ce start testing cloud service implementation 2026-02-03 01:54:19 +05:00
3c08d93bc5 drop everything 2026-02-03 01:13:50 +05:00
42 changed files with 328 additions and 1922 deletions

View file

@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -1,133 +0,0 @@
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<MetaforceInstaller.Core.Models.ProgressInfo>(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]}";
}
}

View file

@ -1,53 +0,0 @@
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,8 @@
using MetaforceInstaller.Cloud.Models;
namespace MetaforceInstaller.Cloud;
public interface ICloudService
{
Task<List<CloudObject>> GetObjects(string webLink);
}

View file

@ -0,0 +1,181 @@
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<CloudObjectBase>? 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; }
}
/// <summary>
/// Базовый тип элемента из "list". Конкретный тип выбирается по полю "type".
/// </summary>
[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<string> WeblinkList { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; }
}
public sealed class ZipWebLinkResponse
{
[JsonPropertyName("key")]
public string Key { get; init; }
}
/// <summary>
/// Конвертер, который смотрит на поле "type" и десериализует в CloudFolderObject или CloudFileObject.
/// </summary>
public sealed class CloudObjectJsonConverter : JsonConverter<CloudObjectBase>
{
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<CloudFolderObject>(options)
?? throw new JsonException("Failed to deserialize folder object."),
"file" => root.Deserialize<CloudFileObject>(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}'.");
}
}
}

View file

@ -0,0 +1,116 @@
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<List<CloudObject>> GetObjects(string webLink)
{
var dto = await GetFromPublicApi<CloudObjectsResponse>(
path: "/api/v4/public/list",
query: new Dictionary<string, string?>
{
["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<CloudObject>(dto.List.Count);
foreach (var entry in dto.List)
result.Add(Map(entry));
return result;
}
public async Task<string?> GetDownloadLink(IEnumerable<string> webLinks, string outputFileName = "archive")
{
var result = await PostToPublicApi<ZipWebLinkResponse>(
path: "/api/v3/zip/weblink",
query: new Dictionary<string, string?>(),
new ZipWebLinkRequest
{
XEmail = "anonym",
WeblinkList = webLinks.ToList(),
Name = outputFileName
}
);
return result?.Key;
}
private async Task<TResponse?> GetFromPublicApi<TResponse>(string path, IReadOnlyDictionary<string, string?> 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<TResponse>(json, JsonOptions);
}
private async Task<TResponse?> PostToPublicApi<TResponse>(string path, IReadOnlyDictionary<string, string?> 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<TResponse>(json, JsonOptions);
}
private static Uri BuildUri(string path, IReadOnlyDictionary<string, string?> 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
};
}
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,14 @@
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
}

View file

@ -1,7 +0,0 @@
namespace MetaforceInstaller.Core;
public static class Defaults
{
public static readonly string StoragePath =
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + "MetaforceInstaller";
}

View file

@ -1,18 +0,0 @@
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

@ -1,9 +0,0 @@
using MetaforceInstaller.Core.Models;
namespace MetaforceInstaller.Core.Intefaces;
public interface IStorageService
{
AppData Load();
void Save(AppData data);
}

View file

@ -1,32 +0,0 @@
<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">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>AdbWinApi.dll</Link>
</EmbeddedResource>
<EmbeddedResource Include="adb\AdbWinUsbApi.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>AdbWinUsbApi.dll</Link>
</EmbeddedResource>
</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

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

View file

@ -1,6 +0,0 @@
namespace MetaforceInstaller.Core.Models;
public class AppData
{
public List<InstallationData> Installations { get; set; } = new();
}

View file

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

View file

@ -1,9 +0,0 @@
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;
}

View file

@ -1,12 +0,0 @@
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; }
}

View file

@ -1,8 +0,0 @@
namespace MetaforceInstaller.Core.Models;
public class InstallationRequest
{
public string ApkPath { get; set; }
public string ZipPath { get; set; }
public string OutputPath { get; set; }
}

View file

@ -1,19 +0,0 @@
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

@ -1,263 +0,0 @@
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;
private DeviceMonitor? _deviceMonitor;
public event EventHandler<ProgressInfo>? ProgressChanged;
public event EventHandler<string>? StatusChanged;
public EventHandler<DeviceDataEventArgs>? DeviceConnected;
public EventHandler<DeviceDataEventArgs>? DeviceDisconnected;
public EventHandler<DeviceDataEventArgs>? DeviceChanged;
public bool IsDeviceConnected => _deviceData != null && _deviceData.State == DeviceState.Online;
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();
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<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);
}
public void Dispose()
{
if (_deviceMonitor != null)
{
_deviceMonitor.DeviceConnected -= OnDeviceConnected;
_deviceMonitor.DeviceDisconnected -= OnDeviceDisconnected;
_deviceMonitor.DeviceChanged -= OnDeviceChanged;
_deviceMonitor.Dispose();
}
}
}

View file

@ -1,19 +0,0 @@
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

@ -1,35 +0,0 @@
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<AppData>(json) ?? new AppData();
}
public void Save(AppData data)
{
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_storagePath, json);
}
}

View file

@ -1,160 +0,0 @@
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),
};
}
/// <summary>
/// Extracts ZIP archive to a unique folder based on installation GUID
/// </summary>
/// <param name="archive">ZIP archive to extract</param>
/// <param name="baseOutputPath">Base storage path</param>
/// <param name="installationGuid">Unique GUID for this installation</param>
/// <param name="progress">Progress reporter</param>
/// <returns>Full path to the extracted folder</returns>
public static string ExtractZip(
ZipArchive archive,
string baseOutputPath,
Guid installationGuid,
IProgress<double>? 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;
}
/// <summary>
/// Updates InstallationParts paths to reflect the actual extracted location
/// </summary>
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");
}
/// <summary>
/// Finds an entry in archive by name and extension
/// </summary>
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;
}
/// <summary>
/// Finds an executable in archive, excluding crash handlers
/// </summary>
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;
}
}

Binary file not shown.

View file

@ -1,10 +0,0 @@
<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

@ -1,25 +0,0 @@
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();
}
}

View file

@ -1,16 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,31 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,23 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,251 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MetaforceInstaller.UI.Lang {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Add new installation.
/// </summary>
public static string AddInstallation {
get {
return ResourceManager.GetString("AddInstallation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string CancelButton {
get {
return ResourceManager.GetString("CancelButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose .zip.
/// </summary>
public static string ChooseZip {
get {
return ResourceManager.GetString("ChooseZip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connected to.
/// </summary>
public static string ConnectedTo {
get {
return ResourceManager.GetString("ConnectedTo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string Delete {
get {
return ResourceManager.GetString("Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install admin.
/// </summary>
public static string InstallAdminCheckbox {
get {
return ResourceManager.GetString("InstallAdminCheckbox", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install Android admin.
/// </summary>
public static string InstallAndroidAdmin {
get {
return ResourceManager.GetString("InstallAndroidAdmin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string InstallButton {
get {
return ResourceManager.GetString("InstallButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install server.
/// </summary>
public static string InstallServerCheckbox {
get {
return ResourceManager.GetString("InstallServerCheckbox", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install VR client.
/// </summary>
public static string InstallVRClient {
get {
return ResourceManager.GetString("InstallVRClient", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch PC admin.
/// </summary>
public static string LaunchPCAdmin {
get {
return ResourceManager.GetString("LaunchPCAdmin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch server.
/// </summary>
public static string LaunchServer {
get {
return ResourceManager.GetString("LaunchServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name of new installation.
/// </summary>
public static string NameOfNewInstallation {
get {
return ResourceManager.GetString("NameOfNewInstallation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t find Android content.
/// </summary>
public static string NoAndroidContentError {
get {
return ResourceManager.GetString("NoAndroidContentError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t find directory with PC admin.
/// </summary>
public static string NoPCAdminError {
get {
return ResourceManager.GetString("NoPCAdminError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t find directory with server.
/// </summary>
public static string NoServerError {
get {
return ResourceManager.GetString("NoServerError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not connected.
/// </summary>
public static string NotConnected {
get {
return ResourceManager.GetString("NotConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t find any VR clients.
/// </summary>
public static string NoVRClientsError {
get {
return ResourceManager.GetString("NoVRClientsError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t find Windows content.
/// </summary>
public static string NoWindowsContentError {
get {
return ResourceManager.GetString("NoWindowsContentError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save Android admin.
/// </summary>
public static string SaveAndroidAdminCheckbox {
get {
return ResourceManager.GetString("SaveAndroidAdminCheckbox", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save VR client.
/// </summary>
public static string SaveVRClientCheckbox {
get {
return ResourceManager.GetString("SaveVRClientCheckbox", resourceCulture);
}
}
}
}

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LaunchServer" xml:space="preserve">
<value>Launch server</value>
</data>
<data name="LaunchPCAdmin" xml:space="preserve">
<value>Launch PC admin</value>
</data>
<data name="InstallVRClient" xml:space="preserve">
<value>Install VR client</value>
</data>
<data name="InstallAndroidAdmin" xml:space="preserve">
<value>Install Android admin</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="AddInstallation" xml:space="preserve">
<value>Add new installation</value>
</data>
<data name="NotConnected" xml:space="preserve">
<value>Not connected</value>
</data>
<data name="ConnectedTo" xml:space="preserve">
<value>Connected to</value>
</data>
<data name="NameOfNewInstallation" xml:space="preserve">
<value>Name of new installation</value>
</data>
<data name="ChooseZip" xml:space="preserve">
<value>Choose .zip</value>
</data>
<data name="InstallServerCheckbox" xml:space="preserve">
<value>Install server</value>
</data>
<data name="InstallAdminCheckbox" xml:space="preserve">
<value>Install admin</value>
</data>
<data name="SaveAndroidAdminCheckbox" xml:space="preserve">
<value>Save Android admin</value>
</data>
<data name="SaveVRClientCheckbox" xml:space="preserve">
<value>Save VR client</value>
</data>
<data name="InstallButton" xml:space="preserve">
<value>Install</value>
</data>
<data name="CancelButton" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="NoServerError" xml:space="preserve">
<value>Couldn't find directory with server</value>
</data>
<data name="NoPCAdminError" xml:space="preserve">
<value>Couldn't find directory with PC admin</value>
</data>
<data name="NoWindowsContentError" xml:space="preserve">
<value>Couldn't find Windows content</value>
</data>
<data name="NoAndroidContentError" xml:space="preserve">
<value>Couldn't find Android content</value>
</data>
<data name="NoVRClientsError" xml:space="preserve">
<value>Couldn't find any VR clients</value>
</data>
</root>

View file

@ -1,77 +0,0 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LaunchServer" xml:space="preserve">
<value>Запустить сервер</value>
</data>
<data name="LaunchPCAdmin" xml:space="preserve">
<value>Запустить админку</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Удалить</value>
</data>
<data name="InstallAndroidAdmin" xml:space="preserve">
<value>Установить андроид-админку</value>
</data>
<data name="InstallVRClient" xml:space="preserve">
<value>Установить на шлем</value>
</data>
<data name="AddInstallation" xml:space="preserve">
<value>Добавить версию</value>
</data>
<data name="ConnectedTo" xml:space="preserve">
<value>Подключено:</value>
</data>
<data name="NotConnected" xml:space="preserve">
<value>Не подключено</value>
</data>
<data name="ChooseZip" xml:space="preserve">
<value>Выбрать .zip</value>
</data>
<data name="CancelButton" xml:space="preserve">
<value>Отмена</value>
</data>
<data name="InstallAdminCheckbox" xml:space="preserve">
<value>Установить админку</value>
</data>
<data name="InstallButton" xml:space="preserve">
<value>Установить</value>
</data>
<data name="InstallServerCheckbox" xml:space="preserve">
<value>Установить сервер</value>
</data>
<data name="NameOfNewInstallation" xml:space="preserve">
<value>Название версии</value>
</data>
<data name="SaveAndroidAdminCheckbox" xml:space="preserve">
<value>Сохранить андроид-админку</value>
</data>
<data name="SaveVRClientCheckbox" xml:space="preserve">
<value>Сохранить VR-клиенты</value>
</data>
<data name="NoAndroidContentError" xml:space="preserve">
<value>Не удалось обнаружить Андроид контент</value>
</data>
<data name="NoPCAdminError" xml:space="preserve">
<value>Не удалось обнаружить ПК-админку</value>
</data>
<data name="NoServerError" xml:space="preserve">
<value>Не удалось обнаружить сервер</value>
</data>
<data name="NoVRClientsError" xml:space="preserve">
<value>Не удалось обнаружить VR-клиенты</value>
</data>
<data name="NoWindowsContentError" xml:space="preserve">
<value>Не удалось обнаружить Windows контент</value>
</data>
</root>

View file

@ -1,52 +0,0 @@
<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>2.0.0-b1</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>
<ItemGroup>
<EmbeddedResource Update="Lang\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Lang\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View file

@ -1,21 +0,0 @@
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

@ -1,65 +0,0 @@
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<InstallationData> 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<InstallationData> 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));
}
}

View file

@ -1,89 +0,0 @@
<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"
xmlns:vm="using:MetaforceInstaller.UI.ViewModels"
xmlns:lang="using:MetaforceInstaller.UI.Lang"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:CompileBindings="True"
x:DataType="vm:MainWindowViewModel"
x:Class="MetaforceInstaller.UI.Windows.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>
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="96" />
<RowDefinition Height="*" />
<RowDefinition Height="24" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Margin="20"
VerticalAlignment="Center"
Source="{DynamicResource Logo}"
HorizontalAlignment="Left">
</Image>
<Button Grid.Row="0" Name="NewInstallationButton" Margin="20" VerticalAlignment="Center"
HorizontalAlignment="Right">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="+" />
<TextBlock Text="{x:Static lang:Resources.AddInstallation}" />
</StackPanel>
</Button.Content>
</Button>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Installations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Title}" Margin="16, 8" HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Id, StringFormat='ID: {0}'}" />
<Label Content="{Binding InstalledAt, StringFormat='Installed: {0}'}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="LaunchServerButton" Click="OnLaunchServerClick"
Content="{x:Static lang:Resources.LaunchServer}" />
<Button Name="LaunchPcAdminButton" Click="OnLaunchAdminClick"
Content="{x:Static lang:Resources.LaunchPCAdmin}" />
<Button Name="InstallVrClientButton"
Content="{x:Static lang:Resources.InstallVRClient}" />
<Button Name="InstallAndroidAdminButton"
Content="{x:Static lang:Resources.InstallAndroidAdmin}" />
<Button Name="DeleteButton" Click="OnDeleteInstallationClick"
Content="{x:Static lang:Resources.Delete}" />
</StackPanel>
</StackPanel>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<DockPanel Grid.Row="2">
<Border Name="StatusCircle" Margin="4" Width="16" Height="16" Background="{Binding StatusColor}"
CornerRadius="50" HorizontalAlignment="Left" />
<Label Name="SerialNumberLabel" Content="{Binding StatusText}" />
<Label Name="VersionLabel" HorizontalAlignment="Right" VerticalAlignment="Center" />
</DockPanel>
</Grid>
</Window>

View file

@ -1,150 +0,0 @@
using System;
using System.Diagnostics;
using System.Reflection;
using AdvancedSharpAdbClient.Models;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using MetaforceInstaller.Core.Intefaces;
using MetaforceInstaller.Core.Models;
using MetaforceInstaller.Core.Services;
using MetaforceInstaller.UI.ViewModels;
namespace MetaforceInstaller.UI.Windows;
public partial class MainWindow : Window
{
private MainWindowViewModel _viewModel;
private IStorageService _storageService;
private AdbService _adbService;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
_storageService = new StorageService();
_adbService = new AdbService();
DataContext = _viewModel;
VersionLabel.Content = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version;
NewInstallationButton.Click += OnNewInstalltionClick;
_adbService.DeviceConnected += OnAdbDeviceConnected;
_adbService.DeviceDisconnected += OnAdbDeviceDisconnected;
_adbService.DeviceChanged += OnAdbDeviceChanged;
LoadInstallations();
UpdateDeviceStatus();
}
private void UpdateDeviceStatus()
{
var isConnected = _adbService.IsDeviceConnected;
_viewModel.IsDeviceConnected = isConnected;
if (isConnected)
{
try
{
var deviceInfo = _adbService.GetDeviceInfo();
_viewModel.DeviceSerial = deviceInfo.SerialNumber;
}
catch
{
_viewModel.DeviceSerial = "Unknown";
}
}
else
{
_viewModel.DeviceSerial = string.Empty;
}
}
private void OnAdbDeviceConnected(object? sender, DeviceDataEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
UpdateDeviceStatus();
});
}
private void OnAdbDeviceDisconnected(object? sender, DeviceDataEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
UpdateDeviceStatus();
});
}
private void OnAdbDeviceChanged(object? sender, DeviceDataEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
UpdateDeviceStatus();
});
}
private void LoadInstallations()
{
var appData = _storageService.Load();
_viewModel.LoadInstallations(appData.Installations);
}
public async void OnNewInstalltionClick(object? sender, RoutedEventArgs e)
{
var newInstallationDialog = new NewInstallationDialog(_storageService);
await newInstallationDialog.ShowDialog<NewInstallationDialog>(this);
LoadInstallations();
}
public async void OnDeleteInstallationClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is InstallationData installationData)
{
var name = installationData.Title;
Console.WriteLine($"Delete {name}");
}
}
public async void OnLaunchServerClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is InstallationData installationData)
{
var exePath = installationData.Parts.WindowsServerPath;
var processInfo = new ProcessStartInfo
{
FileName = exePath,
UseShellExecute = false,
};
Process.Start(processInfo);
}
}
public async void OnLaunchAdminClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is InstallationData installationData)
{
var exePath = installationData.Parts.WindowsAdminPath;
var processInfo = new ProcessStartInfo
{
FileName = exePath,
UseShellExecute = false,
};
Process.Start(processInfo);
}
}
protected override void OnClosed(EventArgs e)
{
_adbService.DeviceConnected -= OnAdbDeviceConnected;
_adbService.DeviceDisconnected -= OnAdbDeviceDisconnected;
_adbService.DeviceChanged -= OnAdbDeviceChanged;
_adbService.Dispose();
base.OnClosed(e);
}
}

View file

@ -1,28 +0,0 @@
<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"
xmlns:lang="clr-namespace:MetaforceInstaller.UI.Lang"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="400"
x:Class="MetaforceInstaller.UI.Windows.NewInstallationDialog"
Title="MetaforceInstaller - Add new installation"
SizeToContent="WidthAndHeight"
CanResize="False">
<StackPanel Margin="24" Spacing="12">
<Label FontSize="36" Content="{x:Static lang:Resources.AddInstallation}" />
<TextBox Name="TitleTextBox" Watermark="{x:Static lang:Resources.NameOfNewInstallation}" />
<Button Name="ChooseZip" Content="{x:Static lang:Resources.ChooseZip}" />
<CheckBox Name="ServerCheckBox" Content="{x:Static lang:Resources.InstallServerCheckbox}" />
<CheckBox Name="PcAdminCheckBox" Content="{x:Static lang:Resources.InstallAdminCheckbox}" />
<CheckBox Name="AndroidAdminCheckbox" Content="{x:Static lang:Resources.SaveAndroidAdminCheckbox}" />
<CheckBox Name="VrClientCheckbox" Content="{x:Static lang:Resources.SaveVRClientCheckbox}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Spacing="8">
<Button Name="InstallButton" Content="{x:Static lang:Resources.InstallButton}" />
<Button Name="CancelButton" Content="{x:Static lang:Resources.CancelButton}" />
</StackPanel>
<ProgressBar Name="ProgressBar" Minimum="0" Maximum="100" Value="0" />
</StackPanel>
</Window>

View file

@ -1,164 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using MetaforceInstaller.Core;
using MetaforceInstaller.Core.Intefaces;
using MetaforceInstaller.Core.Models;
using MetaforceInstaller.Core.Services;
namespace MetaforceInstaller.UI.Windows;
public partial class NewInstallationDialog : Window
{
private string? _zipPath;
private InstallationParts? _installationParts;
private readonly IStorageService _storageService;
public NewInstallationDialog(IStorageService storageService)
{
InitializeComponent();
_storageService = storageService;
RefreshCheckboxes();
CancelButton.Click += OnCancelClick;
ChooseZip.Click += OnChooseZipClick;
InstallButton.IsEnabled = false;
InstallButton.Click += OnInstallClick;
}
private void RefreshCheckboxes()
{
var serverCheckbox = ServerCheckBox;
var pcAdminCheckbox = PcAdminCheckBox;
var androidAdminCheckbox = AndroidAdminCheckbox;
var vrClientCheckbox = VrClientCheckbox;
serverCheckbox.Content = Lang.Resources.InstallServerCheckbox;
serverCheckbox.IsEnabled = true;
pcAdminCheckbox.Content = Lang.Resources.InstallAdminCheckbox;
pcAdminCheckbox.IsEnabled = true;
androidAdminCheckbox.Content = Lang.Resources.SaveAndroidAdminCheckbox;
androidAdminCheckbox.IsEnabled = true;
vrClientCheckbox.Content = Lang.Resources.SaveVRClientCheckbox;
vrClientCheckbox.IsEnabled = true;
}
private async void OnChooseZipClick(object? sender, RoutedEventArgs e)
{
var topLevel = GetTopLevel(this);
var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Выберите архив с контентом",
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType("ZIP Files")
{
Patterns = ["*.zip"]
}
]
});
if (files.Count >= 1)
{
_zipPath = files[0].Path.LocalPath;
using var archive = ZipFile.OpenRead(_zipPath);
_installationParts = ZipScrapper.PeekFiles(archive);
UpdateCheckboxes();
archive.Dispose();
}
}
private async void OnInstallClick(object? sender, RoutedEventArgs e)
{
using var archive = ZipFile.OpenRead(_zipPath);
var title = TitleTextBox.Text ?? Path.GetFileNameWithoutExtension(_zipPath);
var installationGuid = Guid.NewGuid();
var progress = new Progress<double>(value => { ProgressBar.Value = value; });
string extractedPath = null;
await Task.Run(() =>
{
extractedPath = ZipScrapper.ExtractZip(
archive,
Defaults.StoragePath,
installationGuid,
progress);
});
InstallButton.IsEnabled = false;
var appData = _storageService.Load();
var updatedParts = ZipScrapper.UpdatePathsAfterExtraction(_installationParts, extractedPath);
var installationData = new InstallationData
{
Id = installationGuid,
Title = title,
Parts = updatedParts
};
appData.Installations.Add(installationData);
_storageService.Save(appData);
Close();
}
private void UpdateCheckboxes()
{
RefreshCheckboxes();
var serverCheckbox = ServerCheckBox;
var pcAdminCheckbox = PcAdminCheckBox;
var androidAdminCheckbox = AndroidAdminCheckbox;
var vrClientCheckbox = VrClientCheckbox;
if (string.IsNullOrEmpty(_installationParts.WindowsServerPath))
{
serverCheckbox.IsEnabled = false;
serverCheckbox.Content += $"\n{Lang.Resources.NoServerError}";
}
if (string.IsNullOrEmpty(_installationParts.WindowsAdminPath))
{
pcAdminCheckbox.IsEnabled = false;
pcAdminCheckbox.Content += $"\n{Lang.Resources.NoPCAdminError}";
}
if (string.IsNullOrEmpty(_installationParts.WindowsContentPath))
{
pcAdminCheckbox.IsEnabled = false;
pcAdminCheckbox.Content += $"\n{Lang.Resources.NoWindowsContentError}";
}
if (string.IsNullOrEmpty(_installationParts.AndroidContentPath))
{
vrClientCheckbox.IsEnabled = false;
vrClientCheckbox.Content += $"\n{Lang.Resources.NoAndroidContentError}";
androidAdminCheckbox.IsEnabled = false;
androidAdminCheckbox.Content += $"\n{Lang.Resources.NoAndroidContentError}";
}
if (string.IsNullOrEmpty(_installationParts.PicoClientPath) &&
string.IsNullOrEmpty(_installationParts.OculusClientPath))
{
vrClientCheckbox.IsEnabled = false;
vrClientCheckbox.Content += $"\n{Lang.Resources.NoVRClientsError}";
}
InstallButton.IsEnabled = new List<CheckBox?>
{ serverCheckbox, pcAdminCheckbox, androidAdminCheckbox, vrClientCheckbox }.Any(x => x?.IsEnabled == true);
}
private void OnCancelClick(object? sender, RoutedEventArgs e)
{
Close();
}
}

View file

@ -1,18 +0,0 @@
<?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>