2 Commits
Author SHA1 Message Date
kirieska 3d5fa5ef0f доработаный 2026-09-14 21:03:46 +02:00
kirieska 263be52245 доработаный 2026-09-14 21:02:45 +02:00
4 changed files with 726 additions and 181 deletions
+314 -85
View File
@@ -11,149 +11,378 @@ namespace SpotXInstaller.Services;
public sealed class InstallerService public sealed class InstallerService
{ {
private const string BashUrl = "https://api.kirieska.xyz/files/SPOTX/spotx.sh"; private const string BashUrl =
private const string PsUrl = "https://api.kirieska.xyz/files/SPOTX/run.ps1"; "https://api.kirieska.xyz/files/SPOTX/spotx.sh";
private const string PsUninstallUrl = "https://api.kirieska.xyz/files/SPOTX/uninstall.ps1";
public Task<int> InstallAsync(InstallOptions o, private const string PsUrl =
"https://api.kirieska.xyz/files/SPOTX/run.ps1";
private const string PsUninstallUrl =
"https://api.kirieska.xyz/files/SPOTX/uninstall.ps1";
public Task<int> InstallAsync(
InstallOptions options,
IProgress<(int Percent, string Status)> progress, IProgress<(int Percent, string Status)> progress,
IProgress<string> log, CancellationToken ct = default) IProgress<string> log,
=> RunAsync(o, isUninstall: false, progress, log, ct); CancellationToken ct = default)
{
return RunAsync(
options,
isUninstall: false,
progress,
log,
ct);
}
public Task<int> UninstallAsync( public Task<int> UninstallAsync(
IProgress<(int Percent, string Status)> progress, IProgress<(int Percent, string Status)> progress,
IProgress<string> log, CancellationToken ct = default) IProgress<string> log,
=> RunAsync(new InstallOptions(false, false, false, false), CancellationToken ct = default)
isUninstall: true, progress, log, ct);
private async Task<int> RunAsync(InstallOptions o, bool isUninstall,
IProgress<(int, string)> progress, IProgress<string> log, CancellationToken ct)
{ {
bool isWin = OperatingSystem.IsWindows(); return RunAsync(
new InstallOptions(false, false, false, false),
isUninstall: true,
progress,
log,
ct);
}
private async Task<int> RunAsync(
InstallOptions options,
bool isUninstall,
IProgress<(int Percent, string Status)> progress,
IProgress<string> log,
CancellationToken ct)
{
bool isWindows = OperatingSystem.IsWindows();
string url = isUninstall string url = isUninstall
? (isWin ? PsUninstallUrl : BashUrl) ? (isWindows ? PsUninstallUrl : BashUrl)
: (isWin ? PsUrl : BashUrl); : (isWindows ? PsUrl : BashUrl);
string ext = isWin ? ".ps1" : ".sh";
string scriptPath = Path.Combine(Path.GetTempPath(), $"spotx_{Guid.NewGuid():N}{ext}"); string extension = isWindows ? ".ps1" : ".sh";
string scriptPath = Path.Combine(
Path.GetTempPath(),
$"spotx_{Guid.NewGuid():N}{extension}");
try try
{ {
// === ШАГ 1 (0–20%): скачивание === ct.ThrowIfCancellationRequested();
progress.Report((3, "Загрузка скрипта..."));
using (var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) }) // ---------------------------------------------------------
// 1. DOWNLOAD
// ---------------------------------------------------------
progress.Report((5, "Загрузка скрипта..."));
using (var http = new HttpClient
{ {
http.DefaultRequestHeaders.UserAgent.ParseAdd("SpotXInstaller/1.0"); Timeout = TimeSpan.FromSeconds(60)
byte[] data = await http.GetByteArrayAsync(url, ct).ConfigureAwait(false); })
await File.WriteAllBytesAsync(scriptPath, data, ct).ConfigureAwait(false); {
http.DefaultRequestHeaders.UserAgent.ParseAdd(
"SpotXInstaller/1.0");
byte[] data = await http.GetByteArrayAsync(
url,
ct);
await File.WriteAllBytesAsync(
scriptPath,
data,
ct);
} }
progress.Report((20, "Скрипт загружен")); progress.Report((20, "Скрипт загружен"));
if (!isWin) ct.ThrowIfCancellationRequested();
// ---------------------------------------------------------
// 2. UNIX PERMISSIONS
// ---------------------------------------------------------
if (!isWindows)
{ {
progress.Report((25, "Установка прав на исполнение...")); progress.Report(
await ExecAsync("/bin/chmod", new[] { "+x", scriptPath }, _ => { }, ct) (24, "Подготовка скрипта..."));
.ConfigureAwait(false);
int chmodExit = await ExecAsync(
"/bin/chmod",
new[]
{
"+x",
scriptPath
},
_ => { },
ct);
if (chmodExit != 0)
{
throw new InvalidOperationException(
$"Не удалось установить права на скрипт. Код: {chmodExit}");
}
} }
progress.Report((30, isUninstall ? "Восстановление..." : "Запуск установки..."));
// === ШАГ 2: маппинг флагов === ct.ThrowIfCancellationRequested();
// ---------------------------------------------------------
// 3. BUILD COMMAND
// ---------------------------------------------------------
string file; string file;
var argv = new List<string>(); var arguments = new List<string>();
if (isWin) if (isWindows)
{ {
file = "powershell.exe"; file = "powershell.exe";
argv.AddRange(new[]
{
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", scriptPath
});
if (isUninstall) arguments.Add("-NoProfile");
arguments.Add("-ExecutionPolicy");
arguments.Add("Bypass");
arguments.Add("-File");
arguments.Add(scriptPath);
if (!isUninstall)
{ {
// uninstall.ps1 не принимает флагов — просто отрабатывает if (options.BlockUpdates)
} arguments.Add("-block_update_on");
else
{ if (options.HidePodcasts)
if (o.BlockUpdates) argv.Add("-block_update_on"); arguments.Add("-podcasts_off");
if (o.HidePodcasts) argv.Add("-podcasts_off");
if (o.HidePremium) argv.Add("-adsections_off"); if (options.HidePremium)
argv.Add("-no_pause"); // не ждать Enter в конце arguments.Add("-adsections_off");
arguments.Add("-no_pause");
} }
} }
else else
{ {
file = "/bin/bash"; file = "/bin/bash";
argv.Add(scriptPath);
arguments.Add(scriptPath);
if (isUninstall) if (isUninstall)
{ {
argv.Add("--uninstall"); arguments.Add("--uninstall");
} }
else else
{ {
if (o.BlockUpdates) argv.Add("-B"); if (options.BlockUpdates)
if (o.HidePodcasts) argv.Add("-h"); arguments.Add("-B");
if (o.HidePremium) argv.Add("-e");
if (o.ClearCache) argv.Add("-c"); if (options.HidePodcasts)
arguments.Add("-h");
if (options.HidePremium)
arguments.Add("-e");
if (options.ClearCache)
arguments.Add("-c");
} }
} }
// === ШАГ 3 (30–95%): запуск + построчный стрим === // ---------------------------------------------------------
int cur = 30; // 4. RUN
var gate = new object(); // ---------------------------------------------------------
int exit = await ExecAsync(file, argv.ToArray(), line => progress.Report((
30,
isUninstall
? "Запуск восстановления..."
: "Запуск установки..."));
int exitCode = await ExecAsync(
file,
arguments.ToArray(),
line =>
{ {
if (string.IsNullOrWhiteSpace(line))
return;
log.Report(line); log.Report(line);
lock (gate)
{
cur = Math.Min(cur + 2, 95);
progress.Report((cur, isUninstall ? "Восстановление..." : "Установка..."));
}
}, ct).ConfigureAwait(false);
progress.Report((100, "Готово")); string lower = line.ToLowerInvariant();
return exit;
// Никакого фальшивого +2%.
// Только изменение текста статуса по реальному выводу.
if (lower.Contains("download") ||
lower.Contains("скач"))
{
progress.Report(
(40, "Загрузка Spotify..."));
}
else if (lower.Contains("install") ||
lower.Contains("установ"))
{
progress.Report(
(60, "Установка Spotify..."));
}
else if (lower.Contains("patch") ||
lower.Contains("патч"))
{
progress.Report(
(80, "Применение SpotX..."));
}
else if (lower.Contains("complete") ||
lower.Contains("готов"))
{
progress.Report(
(90, "Завершение..."));
}
},
ct);
ct.ThrowIfCancellationRequested();
// ---------------------------------------------------------
// 5. RESULT
// ---------------------------------------------------------
if (exitCode == 0)
{
progress.Report(
(100,
isUninstall
? "Восстановление завершено"
: "Установка завершена"));
}
else
{
progress.Report(
(100,
$"Ошибка установки (код {exitCode})"));
}
return exitCode;
} }
finally finally
{ {
try { if (File.Exists(scriptPath)) File.Delete(scriptPath); } catch { } try
{
if (File.Exists(scriptPath))
File.Delete(scriptPath);
}
catch
{
// Игнорируем ошибку очистки временного файла.
}
} }
} }
private static Task<int> ExecAsync(string file, string[] argv, private static async Task<int> ExecAsync(
Action<string> onLine, CancellationToken ct) string file,
string[] arguments,
Action<string> onLine,
CancellationToken ct)
{ {
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs =
new TaskCompletionSource<int>(
TaskCreationOptions.RunContinuationsAsynchronously);
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = file, FileName = file,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true
};
foreach (var a in argv) psi.ArgumentList.Add(a);
var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
proc.OutputDataReceived += (_, e) => { if (e.Data is { } s) onLine(s); };
proc.ErrorDataReceived += (_, e) => { if (e.Data is { } s) onLine(s); };
proc.Exited += (_, _) =>
{
try { tcs.TrySetResult(proc.ExitCode); }
finally { proc.Dispose(); }
}; };
using var reg = ct.Register(() => foreach (string argument in arguments)
psi.ArgumentList.Add(argument);
var process = new Process
{ {
try { if (!proc.HasExited) proc.Kill(entireProcessTree: true); } StartInfo = psi,
catch { } EnableRaisingEvents = true
};
int finished = 0;
process.OutputDataReceived += (_, e) =>
{
if (e.Data is not null)
onLine(e.Data);
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data is not null)
onLine(e.Data);
};
process.Exited += (_, _) =>
{
if (Interlocked.Exchange(ref finished, 1) != 0)
return;
try
{
tcs.TrySetResult(process.ExitCode);
}
catch
{
tcs.TrySetResult(-1);
}
finally
{
process.Dispose();
}
};
using CancellationTokenRegistration registration =
ct.Register(() =>
{
if (Interlocked.Exchange(ref finished, 1) != 0)
return;
try
{
if (!process.HasExited)
{
process.Kill(
entireProcessTree: true);
}
}
catch
{
// Процесс уже мог завершиться.
}
tcs.TrySetCanceled(ct);
}); });
proc.Start(); try
proc.BeginOutputReadLine(); {
proc.BeginErrorReadLine(); ct.ThrowIfCancellationRequested();
return tcs.Task;
if (!process.Start())
throw new InvalidOperationException(
$"Не удалось запустить процесс: {file}");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return await tcs.Task.ConfigureAwait(false);
}
catch
{
try
{
if (!process.HasExited)
process.Kill(entireProcessTree: true);
}
catch
{
// Ignore.
}
throw;
} }
} }
}
+39 -1
View File
@@ -1,9 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>SpotXInstaller</AssemblyName>
<RootNamespace>SpotXInstaller</RootNamespace>
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -16,10 +25,39 @@
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" /> <PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.2" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.2" />
<!-- Native Skia assets -->
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="3.119.4" />
<PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="3.119.4" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3"> <PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
</ItemGroup> </ItemGroup>
<!-- Windows x64 -->
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<!-- Linux x64 -->
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<!-- macOS Intel -->
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64'">
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
</Project> </Project>
+285 -39
View File
@@ -11,40 +11,102 @@ namespace SpotXInstaller.ViewModels;
public partial class MainViewModel : ViewModelBase public partial class MainViewModel : ViewModelBase
{ {
private readonly InstallerService _installer = new(); private readonly InstallerService _installer = new();
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
// --- СОСТОЯНИЯ ЭКРАНОВ --- // ---------------------------------------------------------
[ObservableProperty] private bool _isStepLoading = true; // SCREEN STATES
[ObservableProperty] private bool _isStepHome; // ---------------------------------------------------------
[ObservableProperty] private bool _isStepSettings;
[ObservableProperty] private bool _isStepProgress;
// --- POPUP --- [ObservableProperty]
[ObservableProperty] private bool _showPopup; private bool _isStepLoading = true;
[ObservableProperty] private string _popupTitle = "";
[ObservableProperty] private string _popupMessage = "";
[ObservableProperty] private string _popupColor = "#8B5CF6";
// --- ГАЛОЧКИ --- [ObservableProperty]
[ObservableProperty] private bool _blockUpdates = true; private bool _isStepHome;
[ObservableProperty] private bool _hidePodcasts = true;
[ObservableProperty] private bool _hidePremium = true;
[ObservableProperty] private bool _clearCache;
// --- ПРОГРЕСС --- [ObservableProperty]
[ObservableProperty] private double _progressValue; private bool _isStepSettings;
[ObservableProperty] private string _progressStatus = "Подготовка...";
[ObservableProperty] private string _miniLog = "";
public MainViewModel() => _ = BootAsync(); [ObservableProperty]
private bool _isStepProgress;
// ---------------------------------------------------------
// POPUP
// ---------------------------------------------------------
[ObservableProperty]
private bool _showPopup;
[ObservableProperty]
private string _popupTitle = "";
[ObservableProperty]
private string _popupMessage = "";
[ObservableProperty]
private string _popupColor = "#8B5CF6";
// ---------------------------------------------------------
// SETTINGS
// ---------------------------------------------------------
[ObservableProperty]
private bool _blockUpdates = true;
[ObservableProperty]
private bool _hidePodcasts = true;
[ObservableProperty]
private bool _hidePremium = true;
[ObservableProperty]
private bool _clearCache;
// ---------------------------------------------------------
// PROGRESS
// ---------------------------------------------------------
[ObservableProperty]
private double _progressValue;
[ObservableProperty]
private string _progressStatus = "Подготовка...";
[ObservableProperty]
private string _miniLog = "";
// ---------------------------------------------------------
// STATE
// ---------------------------------------------------------
[ObservableProperty]
private bool _isInstalling;
public MainViewModel()
{
_ = BootAsync();
}
// ---------------------------------------------------------
// BOOT
// ---------------------------------------------------------
private async Task BootAsync() private async Task BootAsync()
{ {
await Task.Delay(3000); await Task.Delay(3000);
SwitchTo(home: true); SwitchTo(home: true);
} }
private void SwitchTo(bool loading = false, bool home = false, bool settings = false, bool progress = false) // ---------------------------------------------------------
// NAVIGATION
// ---------------------------------------------------------
private void SwitchTo(
bool loading = false,
bool home = false,
bool settings = false,
bool progress = false)
{ {
IsStepLoading = loading; IsStepLoading = loading;
IsStepHome = home; IsStepHome = home;
@@ -52,9 +114,33 @@ public partial class MainViewModel : ViewModelBase
IsStepProgress = progress; IsStepProgress = progress;
} }
[RelayCommand] private void GoToSettings() => SwitchTo(settings: true); [RelayCommand]
[RelayCommand] private void GoBackToHome() => SwitchTo(home: true); private void GoToSettings()
[RelayCommand] private void ClosePopup() => ShowPopup = false; {
if (IsInstalling)
return;
SwitchTo(settings: true);
}
[RelayCommand]
private void GoBackToHome()
{
if (IsInstalling)
return;
SwitchTo(home: true);
}
[RelayCommand]
private void ClosePopup()
{
ShowPopup = false;
}
// ---------------------------------------------------------
// INSTALL
// ---------------------------------------------------------
[RelayCommand] [RelayCommand]
private async Task StartInstallAsync() private async Task StartInstallAsync()
@@ -62,68 +148,228 @@ public partial class MainViewModel : ViewModelBase
await RunInstallerAsync(isUninstall: false); await RunInstallerAsync(isUninstall: false);
} }
// ---------------------------------------------------------
// UNINSTALL
// ---------------------------------------------------------
[RelayCommand] [RelayCommand]
private async Task StartUninstallAsync() private async Task StartUninstallAsync()
{ {
await RunInstallerAsync(isUninstall: true); await RunInstallerAsync(isUninstall: true);
} }
// ---------------------------------------------------------
// CANCEL BUTTON
// ---------------------------------------------------------
[RelayCommand]
private async Task CancelInstallAsync()
{
if (!IsInstalling)
return;
ProgressStatus = "Отмена установки...";
try
{
_cts?.Cancel();
}
catch
{
// Ignore.
}
await Task.CompletedTask;
}
// ---------------------------------------------------------
// WINDOW CLOSE
// ---------------------------------------------------------
public async Task CancelInstallationAsync()
{
if (!IsInstalling)
return;
try
{
ProgressStatus = "Завершение процессов...";
_cts?.Cancel();
// Даём process tree время корректно завершиться.
await Task.Delay(150);
}
catch
{
// Ignore.
}
}
// ---------------------------------------------------------
// MAIN INSTALLER
// ---------------------------------------------------------
private async Task RunInstallerAsync(bool isUninstall) private async Task RunInstallerAsync(bool isUninstall)
{ {
if (IsInstalling)
return;
ProgressValue = 0; ProgressValue = 0;
MiniLog = ""; MiniLog = "";
ProgressStatus = isUninstall ? "Подготовка восстановления..." : "Подготовка установки...";
ProgressStatus = isUninstall
? "Подготовка восстановления..."
: "Подготовка установки...";
SwitchTo(progress: true); SwitchTo(progress: true);
_cts?.Dispose(); _cts?.Dispose();
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
var progress = new Progress<(int Percent, string Status)>(p => IsInstalling = true;
var progress =
new Progress<(int Percent, string Status)>(
p =>
{ {
ProgressValue = p.Percent; ProgressValue = p.Percent;
ProgressStatus = p.Status; ProgressStatus = p.Status;
}); });
var log = new Progress<string>(l => MiniLog = l);
var log =
new Progress<string>(
line =>
{
MiniLog = line;
});
try try
{ {
int code = isUninstall int code;
? await _installer.UninstallAsync(progress, log, _cts.Token)
: await _installer.InstallAsync( if (isUninstall)
new InstallOptions(BlockUpdates, HidePodcasts, HidePremium, ClearCache), {
progress, log, _cts.Token); code = await _installer.UninstallAsync(
progress,
log,
_cts.Token);
}
else
{
code = await _installer.InstallAsync(
new InstallOptions(
BlockUpdates,
HidePodcasts,
HidePremium,
ClearCache),
progress,
log,
_cts.Token);
}
IsInstalling = false;
SwitchTo(home: true); SwitchTo(home: true);
if (code == 0) if (code == 0)
{
ShowNotification( ShowNotification(
isUninstall ? "ГОТОВО" : "УСПЕХ", isUninstall
? "ГОТОВО"
: "УСПЕХ",
isUninstall isUninstall
? "Оригинальный клиент Spotify восстановлен." ? "Оригинальный клиент Spotify восстановлен."
: "SpotX успешно установлен.", : "SpotX успешно установлен.",
isUninstall ? "#8B5CF6" : "#00E676");
isUninstall
? "#8B5CF6"
: "#00E676");
}
else else
ShowNotification("ОШИБКА", {
$"Скрипт завершился с кодом {code}.\n\n{MiniLog[..Math.Min(200, MiniLog.Length)]}", string errorText =
MiniLog.Length > 500
? MiniLog[..500]
: MiniLog;
ShowNotification(
"ОШИБКА",
$"Скрипт завершился с кодом {code}.\n\n{errorText}",
"#FF3B30"); "#FF3B30");
} }
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
IsInstalling = false;
SwitchTo(home: true); SwitchTo(home: true);
ShowNotification("ОТМЕНЕНО", "Операция прервана.", "#FFB300");
ShowNotification(
"ОТМЕНЕНО",
"Установка была отменена.",
"#FFB300");
} }
catch (Exception ex) catch (Exception ex)
{ {
IsInstalling = false;
SwitchTo(home: true); SwitchTo(home: true);
ShowNotification("ОШИБКА", ex.Message, "#FF3B30");
ShowNotification(
"ОШИБКА",
ex.Message,
"#FF3B30");
}
finally
{
IsInstalling = false;
_cts?.Dispose();
_cts = null;
} }
} }
private void ShowNotification(string title, string message, string hexColor) // ---------------------------------------------------------
// POPUP
// ---------------------------------------------------------
private void ShowNotification(
string title,
string message,
string hexColor)
{ {
PopupTitle = title; PopupTitle = title;
PopupMessage = message; PopupMessage = message;
PopupColor = hexColor; PopupColor = hexColor;
ShowPopup = true; ShowPopup = true;
} }
// ---------------------------------------------------------
// DISPOSE
// ---------------------------------------------------------
public async Task DisposeAsync()
{
if (!IsInstalling)
return;
try
{
_cts?.Cancel();
await Task.Delay(150);
} }
catch
{
// Ignore.
}
_cts?.Dispose();
_cts = null;
IsInstalling = false;
}
}
+32
View File
@@ -1,11 +1,43 @@
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using SpotXInstaller.ViewModels;
namespace SpotXInstaller.Views; namespace SpotXInstaller.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private bool _allowClose;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
Closing += MainWindow_Closing;
}
private async void MainWindow_Closing(
object? sender,
WindowClosingEventArgs e)
{
if (_allowClose)
return;
if (DataContext is not MainViewModel vm)
return;
if (!vm.IsInstalling)
return;
// Не закрываем окно мгновенно.
// Сначала отменяем установку и убиваем process tree.
e.Cancel = true;
await vm.CancelInstallationAsync();
_allowClose = true;
Close();
} }
} }