diff --git a/installer/ElfisaPharmacy.iss b/installer/ElfisaPharmacy.iss
index 6bdad70..ba49fbd 100644
--- a/installer/ElfisaPharmacy.iss
+++ b/installer/ElfisaPharmacy.iss
@@ -3,7 +3,7 @@
#define MyAppName "Электронная Фармация"
#define MyAppBrand "ЭльФиСА"
-#define MyAppVersion "2.0.3"
+#define MyAppVersion "2.0.4"
#define MyAppPublisher "PharmData"
#define MyAppURL "https://cdn.24pharmdata.ru"
#define MyAppSupportURL "https://24pharmdata.ru"
diff --git a/src/Elfisa.Avalonia/Elfisa.Avalonia.csproj b/src/Elfisa.Avalonia/Elfisa.Avalonia.csproj
index bb8e0fe..c55b8ac 100644
--- a/src/Elfisa.Avalonia/Elfisa.Avalonia.csproj
+++ b/src/Elfisa.Avalonia/Elfisa.Avalonia.csproj
@@ -5,9 +5,9 @@
enable
app.manifest
true
- 2.0.3
- 2.0.3.0
- 2.0.3.0
+ 2.0.4
+ 2.0.4.0
+ 2.0.4.0
ЭльФиСА — Электронная Фармация
PharmData
diff --git a/src/Elfisa.Avalonia/ViewModels/UpdateViewModel.cs b/src/Elfisa.Avalonia/ViewModels/UpdateViewModel.cs
new file mode 100644
index 0000000..bc18047
--- /dev/null
+++ b/src/Elfisa.Avalonia/ViewModels/UpdateViewModel.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Avalonia.Controls.ApplicationLifetimes;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Elfisa.Core;
+
+namespace Elfisa.Avalonia.ViewModels;
+
+/// Окно «Доступно обновление»: скачивает установщик и запускает его.
+public partial class UpdateViewModel : ViewModelBase
+{
+ private readonly UpdateInfo _info;
+ private readonly Action _close;
+
+ public UpdateViewModel(UpdateInfo info, Action close)
+ {
+ _info = info;
+ _close = close;
+ }
+
+ public string Message =>
+ $"Установлена версия {_info.CurrentVersion}.\nДоступна новая версия {_info.LatestVersion}.";
+
+ [ObservableProperty] private bool _busy;
+ [ObservableProperty] private double _progress;
+ [ObservableProperty] private string _status = "";
+
+ [RelayCommand]
+ private void Later() => _close();
+
+ [RelayCommand]
+ private async Task UpdateAsync()
+ {
+ // Не-Windows: установщика нет — открываем страницу релиза в браузере.
+ if (!OperatingSystem.IsWindows())
+ {
+ OpenUrl(string.IsNullOrWhiteSpace(_info.ReleaseUrl) ? "https://cdn.24pharmdata.ru" : _info.ReleaseUrl);
+ _close();
+ return;
+ }
+
+ try
+ {
+ Busy = true;
+ Status = "Скачивание обновления…";
+ var progress = new Progress(p =>
+ {
+ Progress = p;
+ Status = $"Скачивание обновления… {p:0}%";
+ });
+ var setupPath = await UpdateService.DownloadSetupAsync(progress);
+
+ Status = "Запуск установки…";
+ LaunchInstallerAndExit(setupPath);
+ }
+ catch (Exception ex)
+ {
+ Busy = false;
+ Status = "Не удалось обновить: " + ex.Message;
+ }
+ }
+
+ /// Тихая установка поверх + перезапуск приложения, затем закрываем себя.
+ private void LaunchInstallerAndExit(string setupPath)
+ {
+ var exe = Process.GetCurrentProcess().MainModule?.FileName;
+ var inner = $"\"{setupPath}\" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART";
+ if (!string.IsNullOrEmpty(exe))
+ inner += $" && start \"\" \"{exe}\"";
+
+ Process.Start(new ProcessStartInfo("cmd.exe")
+ {
+ Arguments = "/c \"" + inner + "\"",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ });
+
+ // Закрываем приложение, чтобы установщик заменил файлы (CloseApplications=yes).
+ if (global::Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime d)
+ d.Shutdown();
+ else
+ Environment.Exit(0);
+ }
+
+ private static void OpenUrl(string url)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
+ }
+ catch { /* игнорируем */ }
+ }
+}
diff --git a/src/Elfisa.Avalonia/Views/ShellView.axaml.cs b/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
index 5aa2e3f..cd91df6 100644
--- a/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
+++ b/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
@@ -1,15 +1,25 @@
+using System;
+using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
using Elfisa.Avalonia.ViewModels;
+using Elfisa.Core;
namespace Elfisa.Avalonia.Views;
public partial class ShellView : UserControl
{
- public ShellView() => AvaloniaXamlLoader.Load(this);
+ private static bool _updateChecked; // один раз за запуск
+
+ public ShellView()
+ {
+ AvaloniaXamlLoader.Load(this);
+ Loaded += OnLoaded;
+ }
private async void OnSettingsClick(object? sender, RoutedEventArgs e)
{
@@ -18,4 +28,28 @@ public partial class ShellView : UserControl
if (owner is not null) await win.ShowDialog(owner);
else win.Show();
}
+
+ // Проверка обновлений при первом появлении шелла. Тихо игнорируем офлайн/ошибки.
+ private async void OnLoaded(object? sender, RoutedEventArgs e)
+ {
+ if (_updateChecked) return;
+ _updateChecked = true;
+
+ try
+ {
+ var current = Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(0, 0, 0, 0);
+ var info = await UpdateService.CheckAsync(current);
+ if (info is not { Available: true }) return;
+
+ await Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ var win = new UpdateWindow();
+ win.DataContext = new UpdateViewModel(info, win.Close);
+ var owner = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+ if (owner is not null) await win.ShowDialog(owner);
+ else win.Show();
+ });
+ }
+ catch { /* обновление не должно мешать работе */ }
+ }
}
diff --git a/src/Elfisa.Avalonia/Views/UpdateWindow.axaml b/src/Elfisa.Avalonia/Views/UpdateWindow.axaml
new file mode 100644
index 0000000..787c72f
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/UpdateWindow.axaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/UpdateWindow.axaml.cs b/src/Elfisa.Avalonia/Views/UpdateWindow.axaml.cs
new file mode 100644
index 0000000..2978091
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/UpdateWindow.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class UpdateWindow : Window
+{
+ public UpdateWindow() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/Elfisa.Core/UpdateService.cs b/src/Elfisa.Core/UpdateService.cs
new file mode 100644
index 0000000..1d963de
--- /dev/null
+++ b/src/Elfisa.Core/UpdateService.cs
@@ -0,0 +1,96 @@
+using System.Net.Http;
+using System.Text.Json;
+
+namespace Elfisa.Core;
+
+/// Результат проверки обновления.
+public sealed class UpdateInfo
+{
+ public bool Available { get; init; }
+ public string CurrentVersion { get; init; } = "";
+ public string LatestVersion { get; init; } = "";
+ public string ReleaseUrl { get; init; } = "";
+ public string SetupUrl { get; init; } = "";
+}
+
+///
+/// Проверка и скачивание обновлений Avalonia-клиента.
+/// Смотрит в Gitea Releases (тот же репозиторий, что и старый WinForms-апдейтер),
+/// установщик тянет с CDN. Никаких токенов — репозиторий и CDN публичны.
+///
+public static class UpdateService
+{
+ private const string ReleasesLatest =
+ "https://git.24pharmdata.ru/api/v1/repos/pharmdata/elfisa-pharmacy/releases/latest";
+
+ /// Стабильный URL установщика на CDN (всегда последняя версия).
+ public const string SetupUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
+
+ /// Проверяет наличие новой версии. Возвращает null при офлайне/ошибке (не мешаем работе).
+ public static async Task CheckAsync(Version current)
+ {
+ try
+ {
+ using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(8) };
+ http.DefaultRequestHeaders.Add("Accept", "application/json");
+ var json = await http.GetStringAsync(ReleasesLatest).ConfigureAwait(false);
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ var tag = root.TryGetProperty("tag_name", out var t) ? t.GetString() : null;
+ var latest = ParseVersion(tag);
+ if (latest is null) return null;
+ var htmlUrl = root.TryGetProperty("html_url", out var h) ? h.GetString() : null;
+ return new UpdateInfo
+ {
+ Available = latest > current,
+ CurrentVersion = current.ToString(),
+ LatestVersion = latest.ToString(),
+ ReleaseUrl = htmlUrl ?? "",
+ SetupUrl = SetupUrl,
+ };
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ /// Скачивает установщик с CDN во временный файл. Возвращает путь.
+ public static async Task DownloadSetupAsync(IProgress? progress, CancellationToken ct = default)
+ {
+ var path = Path.Combine(Path.GetTempPath(), "ElfisaPharmacy-Setup.exe");
+ using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
+ using var resp = await http.GetAsync(SetupUrl, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
+ resp.EnsureSuccessStatusCode();
+ var total = resp.Content.Headers.ContentLength ?? -1;
+ await using var src = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
+ await using var dst = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ var buffer = new byte[81920];
+ long readTotal = 0;
+ int n;
+ while ((n = await src.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
+ {
+ await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false);
+ readTotal += n;
+ if (total > 0) progress?.Report(readTotal * 100.0 / total);
+ }
+ return path;
+ }
+
+ /// Разбирает тег вида v2.0.4 / 2.0.4 / 2.0.4-beta в Version.
+ private static Version? ParseVersion(string? tag)
+ {
+ if (string.IsNullOrWhiteSpace(tag)) return null;
+ tag = tag.Trim();
+ if (tag.StartsWith("v", StringComparison.OrdinalIgnoreCase)) tag = tag[1..];
+ var dash = tag.IndexOf('-');
+ if (dash > 0) tag = tag[..dash];
+ switch (tag.Split('.').Length)
+ {
+ case 1: tag += ".0.0.0"; break;
+ case 2: tag += ".0.0"; break;
+ case 3: tag += ".0"; break;
+ }
+ return Version.TryParse(tag, out var v) ? v : null;
+ }
+}