Compare commits

..

13 commits

29 changed files with 328 additions and 996 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,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,26 +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" />
<EmbeddedResource Include="adb\AdbWinUsbApi.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlphaOmega.ApkReader" Version="2.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AdvancedSharpAdbClient" Version="3.4.14" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
</ItemGroup>
</Project>

View file

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

View file

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

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,206 +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;
public event EventHandler<ProgressInfo>? ProgressChanged;
public event EventHandler<string>? StatusChanged;
public AdbService(ILogger<AdbService>? logger = null)
{
_logger = logger ?? new NullLogger<AdbService>();
var adbPath = GetAdbPath();
var server = new AdbServer();
var serverStatus = server.StartServer(adbPath, restartServerIfNewer: false);
_adbClient = new AdbClient();
RefreshDeviceData();
}
public void RefreshDeviceData()
{
var devices = _adbClient.GetDevices();
_deviceData = devices.FirstOrDefault();
}
private void ExtractResource(string resourceName, string outputPath)
{
_logger.LogInformation($"Extracting resource: {resourceName} to {outputPath}");
using var stream = Assembly.GetAssembly(typeof(AdbService)).GetManifestResourceStream(resourceName);
using var fileStream = File.Create(outputPath);
stream.CopyTo(fileStream);
_logger.LogInformation($"Resource extracted: {resourceName} to {outputPath}");
}
private string GetAdbPath()
{
var tempDir = Path.Combine(Path.GetTempPath(), "MetaforceInstaller", "adb");
Directory.CreateDirectory(tempDir);
var adbPath = Path.Combine(tempDir, "adb.exe");
if (File.Exists(adbPath)) return adbPath;
ExtractResource("MetaforceInstaller.Core.adb.adb.exe", adbPath);
ExtractResource("MetaforceInstaller.Core.adb.AdbWinApi.dll", Path.Combine(tempDir, "AdbWinApi.dll"));
ExtractResource("MetaforceInstaller.Core.adb.AdbWinUsbApi.dll", Path.Combine(tempDir, "AdbWinUsbApi.dll"));
return adbPath;
}
private void OnProgressChanged(ProgressInfo progressInfo)
{
ProgressChanged?.Invoke(this, progressInfo);
}
private void OnStatusChanged(string status)
{
StatusChanged?.Invoke(this, status);
}
public void InstallApk(string apkPath)
{
InstallApkAsync(apkPath).Wait();
}
public async Task InstallApkAsync(string apkPath, IProgress<ProgressInfo>? progress = null,
CancellationToken cancellationToken = default)
{
try
{
if (!File.Exists(apkPath))
{
_logger.LogCritical("Error: Could not find APK file.");
return;
}
OnStatusChanged("Начинаем установку APK...");
_logger.LogInformation($"Installing APK: {apkPath}");
progress?.Report(new ProgressInfo
{
PercentageComplete = 0,
Message = "Подготовка к установке APK...",
Type = ProgressType.Installation,
CurrentFile = Path.GetFileName(apkPath)
});
var packageManager = new PackageManager(_adbClient, _deviceData);
await Task.Run(() =>
{
packageManager.InstallPackage(apkPath, installProgress =>
{
var progressInfo = new ProgressInfo
{
PercentageComplete = (int)installProgress.UploadProgress,
Message = $"Установка APK: {installProgress.UploadProgress:F1}%",
Type = ProgressType.Installation,
CurrentFile = Path.GetFileName(apkPath)
};
progress?.Report(progressInfo);
OnProgressChanged(progressInfo);
});
}, cancellationToken);
OnStatusChanged("APK успешно установлен!");
_logger.LogInformation("APK successfully installed!");
}
catch (Exception ex)
{
_logger.LogCritical($"Error: {ex.Message}");
throw;
}
}
public void CopyFile(string localPath, string remotePath)
{
CopyFileAsync(localPath, remotePath).Wait();
}
public async Task CopyFileAsync(string localPath, string remotePath, IProgress<ProgressInfo>? progress = null,
CancellationToken cancellationToken = default)
{
try
{
if (!File.Exists(localPath))
{
_logger.LogCritical($"Error: Could not find file: {localPath}");
return;
}
OnStatusChanged("Начинаем копирование файла...");
_logger.LogInformation($"Copying file: {localPath} to {remotePath}");
var fileInfo = new FileInfo(localPath);
progress?.Report(new ProgressInfo
{
PercentageComplete = 0,
Message = "Подготовка к копированию файла...",
Type = ProgressType.FileCopy,
CurrentFile = Path.GetFileName(localPath),
TotalBytes = fileInfo.Length
});
var remoteDir = Path.GetDirectoryName(remotePath)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(remoteDir))
{
var reciever = new ConsoleOutputReceiver();
await Task.Run(
() => { _adbClient.ExecuteRemoteCommand($"mkdir -p \"{remoteDir}\"", _deviceData, reciever); },
cancellationToken);
}
_logger.LogInformation($"Ensured remote directory: {remoteDir}");
await Task.Run(() =>
{
using var fileStream = File.OpenRead(localPath);
var syncService = new SyncService(_adbClient, _deviceData);
syncService.Push(fileStream, remotePath, UnixFileStatus.DefaultFileMode, DateTime.Now,
copyProgress =>
{
var progressInfo = new ProgressInfo
{
PercentageComplete = (int)copyProgress.ProgressPercentage,
BytesTransferred = copyProgress.ReceivedBytesSize,
TotalBytes = copyProgress.TotalBytesToReceive,
Message = $"Копирование: {copyProgress.ProgressPercentage:F1}%",
Type = ProgressType.FileCopy,
CurrentFile = Path.GetFileName(localPath)
};
progress?.Report(progressInfo);
OnProgressChanged(progressInfo);
});
}, cancellationToken);
OnStatusChanged("Файл успешно скопирован!");
_logger.LogInformation("File successfully copied!");
}
catch (Exception ex)
{
_logger.LogCritical($"Error: {ex.Message}");
throw;
}
}
public DeviceInfo GetDeviceInfo()
{
return new DeviceInfo(_deviceData.Serial, _deviceData.State.ToString(), _deviceData.Model, _deviceData.Name);
}
}

View file

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

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,23 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace MetaforceInstaller.UI;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

View file

@ -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,70 +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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MetaforceInstaller.UI.MainWindow"
Title="MetaforceInstaller">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SvgImage x:Key="Logo" Source="/Images/logo_red.svg"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SvgImage x:Key="Logo" Source="/Images/logo_red.svg"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Main Content Area -->
<Grid Grid.Row="0" Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" /> <!-- 30% -->
<ColumnDefinition Width="7*" /> <!-- 70% -->
</Grid.ColumnDefinitions>
<!-- Left Panel - Buttons -->
<StackPanel Grid.Column="0" Margin="20" Spacing="15">
<Button Name="ChooseApkButton" Content="Choose .apk"
HorizontalAlignment="Stretch" />
<Button Name="ChooseContentButton" Content="Choose .zip"
HorizontalAlignment="Stretch" />
<Button Name="InstallButton" Content="Install"
HorizontalAlignment="Stretch" />
</StackPanel>
<Image Grid.Column="0"
Margin="20"
VerticalAlignment="Bottom"
Source="{DynamicResource Logo}"
HorizontalAlignment="Center">
</Image>
<!-- Right Panel - Logs -->
<TextBox Grid.Column="1" Name="LogsTextBox"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
FontFamily="Consolas,Courier New,monospace"
Focusable="False"
Margin="4,0,0,0" />
</Grid>
<!-- Progress Bar at Bottom -->
<ProgressBar Grid.Row="1" Name="InstallProgressBar"
Height="20" Minimum="0" Maximum="100" Value="0"
Margin="8" />
</Grid>
</Window>

View file

@ -1,230 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
using MetaforceInstaller.Core.Models;
using MetaforceInstaller.Core.Services;
namespace MetaforceInstaller.UI;
public partial class MainWindow : Window
{
private string? _apkPath;
private string? _zipPath;
private AdbService _adbService;
private const int PROGRESS_LOG_STEP = 10;
private const int PROGRESS_UPDATE_STEP = 1;
private int _lastLoggedProgress = -1;
private int _lastUpdatedProgress = -1;
public MainWindow()
{
InitializeComponent();
LogMessage("MetaforceInstaller by slavagm");
_adbService = new AdbService();
_adbService.ProgressChanged += OnAdbProgressChanged;
_adbService.StatusChanged += OnAdbStatusChanged;
CheckAndEnableInstallButton();
ChooseApkButton.Click += OnChooseApkClicked;
ChooseContentButton.Click += OnChooseContentClicked;
InstallButton.Click += OnInstallClicked;
}
private void OnAdbProgressChanged(object? sender, ProgressInfo e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (e.PercentageComplete != _lastUpdatedProgress &&
e.PercentageComplete % PROGRESS_UPDATE_STEP == 0)
{
InstallProgressBar.Value = e.PercentageComplete;
_lastUpdatedProgress = e.PercentageComplete;
}
if (e.PercentageComplete != _lastLoggedProgress &&
e.PercentageComplete % PROGRESS_LOG_STEP == 0 || e.PercentageComplete == 100)
{
LogMessage(
e.TotalBytes > 0
? $"Прогресс: {e.PercentageComplete}% ({FormatBytes(e.BytesTransferred)} / {FormatBytes(e.TotalBytes)})"
: $"Прогресс: {e.PercentageComplete}%");
_lastLoggedProgress = e.PercentageComplete;
}
});
}
private void OnProgressReport(ProgressInfo progressInfo)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (progressInfo.PercentageComplete != _lastUpdatedProgress &&
progressInfo.PercentageComplete % PROGRESS_UPDATE_STEP == 0)
{
InstallProgressBar.Value = progressInfo.PercentageComplete;
_lastUpdatedProgress = progressInfo.PercentageComplete;
}
if (progressInfo.PercentageComplete != _lastLoggedProgress &&
(progressInfo.PercentageComplete % PROGRESS_LOG_STEP == 0 || progressInfo.PercentageComplete == 100))
{
LogMessage(
progressInfo.TotalBytes > 0
? $"Прогресс: {progressInfo.PercentageComplete}% ({FormatBytes(progressInfo.BytesTransferred)} / {FormatBytes(progressInfo.TotalBytes)})"
: $"Прогресс: {progressInfo.PercentageComplete}%");
_lastLoggedProgress = progressInfo.PercentageComplete;
}
});
}
private void OnAdbStatusChanged(object? sender, string e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
InstallProgressBar.Value = 0;
_lastLoggedProgress = -1;
_lastUpdatedProgress = -1;
LogMessage(e);
});
}
private string FormatBytes(long bytes)
{
string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
var counter = 0;
double number = bytes;
while (Math.Round(number / 1024) >= 1)
{
number /= 1024;
counter++;
}
return $"{number:N1} {suffixes[counter]}";
}
private async void CheckAndEnableInstallButton()
{
InstallButton.IsEnabled = !string.IsNullOrEmpty(_apkPath) && !string.IsNullOrEmpty(_zipPath);
}
private async void OnChooseApkClicked(object? sender, RoutedEventArgs e)
{
var topLevel = GetTopLevel(this);
var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Выберите APK файл",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("APK Files")
{
Patterns = new[] { "*.apk" }
}
}
});
if (files.Count >= 1)
{
_apkPath = files[0].Path.LocalPath;
LogMessage($"APK выбран: {Path.GetFileName(_apkPath)}");
}
CheckAndEnableInstallButton();
}
private async void OnChooseContentClicked(object? sender, RoutedEventArgs e)
{
var topLevel = GetTopLevel(this);
var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Выберите архив с контентом",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("ZIP Files")
{
Patterns = new[] { "*.zip" }
}
}
});
if (files.Count >= 1)
{
_zipPath = files[0].Path.LocalPath;
LogMessage($"Контент выбран: {Path.GetFileName(_zipPath)}");
}
CheckAndEnableInstallButton();
}
private async void OnInstallClicked(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_apkPath) || string.IsNullOrEmpty(_zipPath))
{
LogMessage("Ошибка: Выберите APK файл и папку с контентом");
return;
}
_adbService.RefreshDeviceData();
InstallButton.IsEnabled = false;
InstallProgressBar.Value = 0;
try
{
LogMessage("Начинаем установку...");
var deviceInfo = _adbService.GetDeviceInfo();
LogMessage($"Найдено устройство: {deviceInfo.SerialNumber}");
LogMessage($"Состояние: {deviceInfo.State}");
LogMessage($"Модель: {deviceInfo.Model} - {deviceInfo.Name}");
var progress = new Progress<ProgressInfo>(OnProgressReport);
await _adbService.InstallApkAsync(_apkPath, progress);
var apkInfo = ApkScrapper.GetApkInfo(_apkPath);
LogMessage($"Ставим {apkInfo.PackageName} версии {apkInfo.VersionName}");
var zipName = Path.GetFileName(_zipPath);
var outputPath =
@$"/storage/emulated/0/Android/data/{apkInfo.PackageName}/files/{zipName}";
LogMessage($"Начинаем копирование контента в {outputPath}");
await _adbService.CopyFileAsync(_zipPath, outputPath, progress);
LogMessage("Установка завершена успешно!");
}
catch (Exception ex)
{
LogMessage($"Ошибка установки: {ex.Message}");
}
finally
{
InstallButton.IsEnabled = true;
InstallProgressBar.Value = 0;
}
}
private void LogMessage(string message)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
LogsTextBox.Text += $"[{timestamp}] {message}\n";
var scrollViewer = LogsTextBox.FindAncestorOfType<ScrollViewer>();
scrollViewer?.ScrollToEnd();
}
}

View file

@ -1,37 +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>1.2.1</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.6"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.6"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MetaforceInstaller.Core\MetaforceInstaller.Core.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Images\logo_black.svg" />
<AvaloniaResource Include="Images\logo_white.svg" />
<AvaloniaResource Include="Images\logo_red.svg" />
</ItemGroup>
</Project>

View file

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