using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Livia.Models; using Livia.Models.Data; using Livia.Properties; using Livia.Utility; using Livia.Utility.DependencyInjection; using Livia.Views.Utility; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Livia.ViewModels; public class MainWindowViewModel : ObservableRecipient, IDisposable, IRecipient { public int TransitionerSelectedIndex { get { if (_versionUpdateManager.Updating) return 2; if (_underMaintenance) return 3; return ServerHandler.IsLoggedIn switch { true => 1, _ => 0 }; } } public bool DialogOpen { get => _dialogOpen; set => SetProperty(ref _dialogOpen, value); } public string? MaintenanceMessage { get => _maintenanceMessage; private set => SetProperty(ref _maintenanceMessage, value); } public static bool ConfirmExiting { get; set; } = true; public static bool ShowHistoryButton { get; set; } = true; public static bool ShowRemainingQuota { get; set; } = true; public static bool ShowLogoutButton { get; set; } = true; public static bool ShowLoadDataButton { get; set; } = true; public static bool ShowExportReportButton { get; set; } = true; public static bool ShowCheckReportsButton { get; set; } = true; public static bool ShowSearchButton { get; set; } = true; public static bool HideTitleBar { get; set; } public static bool ClearFilesOnExit { get; set; } = true; public IServerHandler ServerHandler { get; } public IPatientInfoManager PatientInfoManager { get; } public IDataBlockLoader DataBlockLoader { get; } public DataBlockTableViewModel JobListTableViewModel { get; } public DataBlockTableViewModel SearchResultTableViewModel { get; } public SearchQueryInputControlViewModel SearchQueryViewModel { get; } private DataBlock? _currentProcessingDataBlock; private bool _dialogOpen; private bool _underMaintenance; private string? _maintenanceMessage; private readonly ILogger _logger; private readonly IWarningSystem _warningSystem; private readonly IVersionUpdateManager _versionUpdateManager; public MainWindowViewModel(ILogger logger, IWarningSystem warningSystem, IServerHandler serverHandler, IVersionUpdateManager versionUpdateManager, IPatientInfoManager patientInfoManager, IDataBlockLoader dataBlockLoader) { _warningSystem = warningSystem; ServerHandler = serverHandler; _versionUpdateManager = versionUpdateManager; PatientInfoManager = patientInfoManager; DataBlockLoader = dataBlockLoader; _logger = logger; JobListTableViewModel = ActivatorUtilities.GetServiceOrCreateInstance(ServiceProviderFactory.ServiceProvider); SearchResultTableViewModel = ActivatorUtilities.GetServiceOrCreateInstance(ServiceProviderFactory.ServiceProvider); SearchResultTableViewModel.SetSearchResult(); SearchQueryViewModel = ActivatorUtilities.GetServiceOrCreateInstance(ServiceProviderFactory.ServiceProvider); if (!Settings.Default.ApplicationQuitNormally) { _warningSystem.ShowDialog(WarningWindowKind.Warning, false, "ApplicationDidNotQuitNormallyWarning"); } Settings.Default.ApplicationQuitNormally = false; Settings.Default.Save(); _warningSystem = warningSystem; _versionUpdateManager.CheckForUpdate(false).ContinueWith(_ => InstallUpdate()); //update TransitionerSelectedIndex _versionUpdateManager.PropertyChanged += (_, args) => { if (args.PropertyName != nameof(IVersionUpdateManager.Updating)) return; OnPropertyChanged(nameof(TransitionerSelectedIndex)); }; ServerHandler.PropertyChanged += (_, args) => { if (args.PropertyName != nameof(IServerHandler.IsLoggedIn)) return; OnPropertyChanged(nameof(TransitionerSelectedIndex)); }; //create temp folder Directory.CreateDirectory(ServiceConfigurations.TempFolder); WeakReferenceMessenger.Default.RegisterAll(this); //get server notice Task.Run(UpdateNotice); } private async Task UpdateNotice() { ServerNotice? notice = await ServerHandler.GetNotice().ConfigureAwait(false); if (notice == null) return; _underMaintenance = notice.Value.ServerStatus == 0; if (_underMaintenance) { MaintenanceMessage = notice.Value.Content; OnPropertyChanged(nameof(TransitionerSelectedIndex)); } else if (Settings.Default.LastReadNoticeId != notice.Value.Id) { _warningSystem.ShowDialogString(WarningWindowKind.Info, false, notice.Value.Content); Settings.Default.LastReadNoticeId = notice.Value.Id; } } private void InstallUpdate() { if (!_versionUpdateManager.UpdateAvailable) return; bool updateNow = _warningSystem.ShowDialog(WarningWindowKind.Info, true, "NewVersionAvailableInfoMessage") ?? false; if (updateNow) { Task.Run(_versionUpdateManager.UpdateApp); } } public async Task ProcessData(string path, CancellationToken token) { _currentProcessingDataBlock = new DataBlock(path); _logger.LogInformation("Creating data block from {path}", path); (bool success, string messageIndex) = await ServerHandler.ProcessData(_currentProcessingDataBlock, token).ConfigureAwait(false); if (success) { //TODO::this is bad await Application.Current.Dispatcher.Invoke(() => DataBlockLoader.LoadData(_currentProcessingDataBlock)); } else { if (token.IsCancellationRequested) { _logger.LogInformation("Data processing canceled"); } else { _warningSystem.ShowDialog(WarningWindowKind.Error, true, messageIndex); } } } //from https://stackoverflow.com/questions/3625658/how-do-you-create-the-hash-of-a-folder-in-c private static string CreateMd5ForFolder(string path) { // assuming you want to include nested folders List files = Directory.GetFiles(path, "*", SearchOption.AllDirectories).OrderBy(p => p).ToList(); MD5 md5 = MD5.Create(); for (int i = 0; i < files.Count; i++) { string file = files[i]; // hash path string relativePath = file[(path.Length + 1)..]; byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower()); md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); // hash contents byte[] contentBytes = File.ReadAllBytes(file); if (i == files.Count - 1) md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length); else md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0); } return md5.Hash == null ? string.Empty : BitConverter.ToString(md5.Hash).Replace("-", "").ToLower(); } public async Task ProcessDataCommandLine(Dictionary argsDict, CancellationToken token) { string path = argsDict["input_dir"]; string hash; try { hash = CreateMd5ForFolder(path); Directory.CreateDirectory(argsDict["output_dir"]); Directory.CreateDirectory(argsDict["log_dir"]); } catch (Exception e) { _logger.LogError(e, "Error creating MD5 for {dir}", path); _warningSystem.ShowDialog(WarningWindowKind.Error, true, "PathNotFoundError"); return; } _logger.LogInformation("Folder hash = {hash}", hash); if (hash == string.Empty) { _warningSystem.ShowDialog(WarningWindowKind.Error, true, "ServerErrorUnknown"); Application.Current.Dispatcher.Invoke(Application.Current.Shutdown); return; } _currentProcessingDataBlock = new DataBlock(path) { Key = hash, ArgsDict = argsDict }; string resultPath = Path.Combine(ServiceConfigurations.TempFolder, _currentProcessingDataBlock.Key); if (File.Exists(Path.Join(resultPath, "complete"))) { //cache exists, load them _currentProcessingDataBlock.ResultPath = Path.Join(resultPath, "result"); //TODO::this is bad await Application.Current.Dispatcher.Invoke(() => DataBlockLoader.LoadData(_currentProcessingDataBlock)).ConfigureAwait(false); return; } _logger.LogInformation("Creating data block from {path}", path); (bool success, string messageIndex) = await ServerHandler.ProcessData(_currentProcessingDataBlock, token).ConfigureAwait(false); if (success) { //TODO::this is bad await Application.Current.Dispatcher.Invoke(() => DataBlockLoader.LoadData(_currentProcessingDataBlock)).ConfigureAwait(false); } else { if (token.IsCancellationRequested) { _logger.LogInformation("Data processing canceled"); } else { _warningSystem.ShowDialog(WarningWindowKind.Warning, true, messageIndex); } Application.Current.Dispatcher.Invoke(Application.Current.Shutdown); } } public void Dispose() { if (ServerHandler.Processing && _currentProcessingDataBlock != null) { ServerHandler.Cancel(_currentProcessingDataBlock.Key); } //remove everything from temp folder if (ClearFilesOnExit && Directory.Exists(ServiceConfigurations.TempFolder)) { foreach (string file in Directory.GetFiles(ServiceConfigurations.TempFolder)) { try { File.Delete(file); } catch (Exception e) { _logger.LogError(e, "Cannot delete file: {file}", file); } } foreach (string dir in Directory.GetDirectories(ServiceConfigurations.TempFolder)) { try { Directory.Delete(dir, true); } catch (Exception e) { _logger.LogError(e, "Cannot delete directory: {file}", dir); } } } else { _logger.LogInformation("TempFolder does not exist, skipping"); } //clear old logs if (ClearFilesOnExit && Directory.Exists(ServiceConfigurations.TempFolder)) { TimeSpan threshold = TimeSpan.FromDays(30); string[] files = Directory.GetFiles(ServiceProviderFactory.LogFolder); foreach (string file in files) { try { string fileName = Path.GetFileNameWithoutExtension(file); string timeString = Regex.Match(fileName, @"\d+").Value; DateTime logDate = DateTime.ParseExact(timeString, "yyyyMMdd", CultureInfo.InvariantCulture); if (DateTime.Now - logDate < threshold) continue; File.Delete(file); } catch (Exception e) { _logger.LogError(e, "Cannot process file: {file}", file); } } } else { _logger.LogInformation("LogFolder does not exist, skipping"); } Settings.Default.ApplicationQuitNormally = true; Settings.Default.Save(); } public bool ConfirmClosing() { if (!ConfirmExiting) return true; return _warningSystem.ShowDialog(WarningWindowKind.Confirmation, true, "ConfirmExitWarning") ?? true; } public bool ConfirmLogout() { return _warningSystem.ShowDialog(WarningWindowKind.Confirmation, true, "ConfirmLogoutWarning") ?? false; } public void Receive(LogoutMessage message) { DialogOpen = false; _logger.LogInformation("Logging out"); } }