From 2a8bbf76bf619592f2b44544666cc404e26dcd9e Mon Sep 17 00:00:00 2001 From: Michael Babienco Date: Tue, 24 Feb 2026 19:38:20 +0900 Subject: [PATCH] Warn before software closed; fix bug on first run Also put settings into proper dir --- src/App.axaml | 3 + src/Interfaces/ICanCheckShutdown.cs | 8 +++ src/Interfaces/IChangeViewModel.cs | 11 ++- src/Interfaces/ITopLevelGrabber.cs | 9 ++- src/MainWindow.axaml | 3 +- src/MainWindow.axaml.cs | 59 ++++++++++++++++ src/Models/Settings.cs | 2 +- src/Models/ShutdownCheckOptions.cs | 8 +++ src/ViewModels/MainViewModel.cs | 88 ++++++++++++++++++++++-- src/ViewModels/ShutdownCheckViewModel.cs | 29 ++++++++ src/Views/MainView.axaml | 2 +- src/Views/MainView.axaml.cs | 14 ++++ src/Views/ShutdownCheckView.axaml | 39 +++++++++++ src/Views/ShutdownCheckView.axaml.cs | 14 ++++ 14 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 src/Interfaces/ICanCheckShutdown.cs create mode 100644 src/Models/ShutdownCheckOptions.cs create mode 100644 src/ViewModels/ShutdownCheckViewModel.cs create mode 100644 src/Views/ShutdownCheckView.axaml create mode 100644 src/Views/ShutdownCheckView.axaml.cs diff --git a/src/App.axaml b/src/App.axaml index 7c9b12d..f568dc0 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -100,6 +100,9 @@ + + + diff --git a/src/Interfaces/ICanCheckShutdown.cs b/src/Interfaces/ICanCheckShutdown.cs new file mode 100644 index 0000000..ecc9e03 --- /dev/null +++ b/src/Interfaces/ICanCheckShutdown.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace MayShow.Interfaces; + +interface ICanCheckShutdown +{ + Task CheckIsSafeToShutdown(); +} \ No newline at end of file diff --git a/src/Interfaces/IChangeViewModel.cs b/src/Interfaces/IChangeViewModel.cs index d992335..059e810 100644 --- a/src/Interfaces/IChangeViewModel.cs +++ b/src/Interfaces/IChangeViewModel.cs @@ -1,10 +1,9 @@ using MayShow.ViewModels; -namespace MayShow.Interfaces +namespace MayShow.Interfaces; + +interface IChangeViewModel { - interface IChangeViewModel - { - void PushViewModel(BaseViewModel model); - void PopViewModel(); - } + void PushViewModel(BaseViewModel model); + void PopViewModel(); } \ No newline at end of file diff --git a/src/Interfaces/ITopLevelGrabber.cs b/src/Interfaces/ITopLevelGrabber.cs index 9624d49..d58e1c5 100644 --- a/src/Interfaces/ITopLevelGrabber.cs +++ b/src/Interfaces/ITopLevelGrabber.cs @@ -1,9 +1,8 @@ using Avalonia.Controls; -namespace MayShow.Interfaces +namespace MayShow.Interfaces; + +interface ITopLevelGrabber { - interface ITopLevelGrabber - { - TopLevel GetTopLevel(); - } + TopLevel GetTopLevel(); } \ No newline at end of file diff --git a/src/MainWindow.axaml b/src/MainWindow.axaml index dfe33db..3867d1d 100644 --- a/src/MainWindow.axaml +++ b/src/MainWindow.axaml @@ -13,7 +13,8 @@ Height="650" MinHeight="550"> + Identifier="DialogHost" + x:Name="WindowDialogHost"> diff --git a/src/MainWindow.axaml.cs b/src/MainWindow.axaml.cs index 7ef7402..33f39fc 100644 --- a/src/MainWindow.axaml.cs +++ b/src/MainWindow.axaml.cs @@ -1,4 +1,9 @@ +using System; +using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using DialogHostAvalonia; using MayShow.Interfaces; using MayShow.ViewModels; @@ -10,6 +15,60 @@ public partial class MainWindow : Window, ITopLevelGrabber { InitializeComponent(); DataContext = new MainWindowViewModel(this); + + Closing += WindowIsClosing; + + var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + // lifetime?.ShutdownRequested += ApplicationIsShuttingDown; + } + + private async void WindowIsClosing(object? sender, WindowClosingEventArgs e) + { + e.Cancel = true; // async -> need to cancel immediately + if (await CheckIfClosePossible()) + { + Closing -= WindowIsClosing; + var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + lifetime?.ShutdownRequested -= ApplicationIsShuttingDown; + Close(); + } + } + + private async void ApplicationIsShuttingDown(object? sender, ShutdownRequestedEventArgs e) + { + e.Cancel = true; // async -> need to cancel immediately + if (await CheckIfClosePossible()) + { + Closing -= WindowIsClosing; + var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + lifetime?.ShutdownRequested -= ApplicationIsShuttingDown; + lifetime?.TryShutdown(); + } + } + + private async Task CheckIfClosePossible() + { + var canShutdown = true; + if (DataContext is MainWindowViewModel mwvm) + { + if (mwvm is ICanCheckShutdown canCheck) + { + canShutdown = await canCheck.CheckIsSafeToShutdown(); + } + // only checking 1 level but for this app that is OK + if (canShutdown && mwvm.CurrentViewModel is ICanCheckShutdown currModel) + { + try + { + canShutdown = await currModel.CheckIsSafeToShutdown(); + } + catch (Exception) + { + canShutdown = true; + } + } + } + return canShutdown; } public TopLevel GetTopLevel() diff --git a/src/Models/Settings.cs b/src/Models/Settings.cs index a627682..7989f37 100644 --- a/src/Models/Settings.cs +++ b/src/Models/Settings.cs @@ -34,7 +34,7 @@ class Settings : ChangeNotifier { var path = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "ReceiptPDFBuilder" // legacy name for existing settings prior to app name change + "MayShow" ); if (!Directory.Exists(path)) { diff --git a/src/Models/ShutdownCheckOptions.cs b/src/Models/ShutdownCheckOptions.cs new file mode 100644 index 0000000..e76b488 --- /dev/null +++ b/src/Models/ShutdownCheckOptions.cs @@ -0,0 +1,8 @@ +namespace MayShow.Models; + +enum ShutdownCheckOptions +{ + SaveAndShutdown, + NoSaveShutdown, + CancelShutdown, +} \ No newline at end of file diff --git a/src/ViewModels/MainViewModel.cs b/src/ViewModels/MainViewModel.cs index 600baaf..6105d04 100644 --- a/src/ViewModels/MainViewModel.cs +++ b/src/ViewModels/MainViewModel.cs @@ -23,8 +23,9 @@ using MayShows.Helpers; namespace MayShow.ViewModels; -class MainViewModel : BaseViewModel, IFontResolver +class MainViewModel : BaseViewModel, IFontResolver, ICanCheckShutdown { + private bool _isPerformingInitialLoad; private string _processDir; private bool _isCreatingPDF; private string _createPDFLog; @@ -36,8 +37,11 @@ class MainViewModel : BaseViewModel, IFontResolver private Settings _settings; + private bool _hasUnsavedWork; + public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger) { + _isPerformingInitialLoad = true; _processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? ""; Console.WriteLine("Process is running from: {0}", _processDir); _isCreatingPDF = false; @@ -47,7 +51,7 @@ class MainViewModel : BaseViewModel, IFontResolver _createPDFLog = "----- MayShow v" + Constants.AppVersion + " ------" + Environment.NewLine; _createPDFLog += quotes[quoteIndex] + Environment.NewLine; _createPDFLog += "---------------------------------------" + Environment.NewLine; - _createPDFLog += "Ready to create PDF!"; + _createPDFLog += "Loaded and ready to create report!"; _workingFolder = ""; _reportFiles = new ObservableCollection(); _reportFiles.CollectionChanged += ( sender, e ) => { NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); }; @@ -59,12 +63,24 @@ class MainViewModel : BaseViewModel, IFontResolver LogInfo("Loading data at last used path of {0}", _settings.LastUsedPath); ScanFolder(_settings.LastUsedPath); } + else + { + LogInfo("Choose a receipt folder to begin..."); + } + HasUnsavedWork = false; + _isPerformingInitialLoad = false; } public string ReportTitle { get => _reportTitle; - set { _reportTitle = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(IsTitleBoxVisible)); } + set + { + _reportTitle = value; + NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(IsTitleBoxVisible)); + NotifyPropertyChanged(nameof(CanAddItem)); + } } public bool IsTitleBoxVisible @@ -72,6 +88,11 @@ class MainViewModel : BaseViewModel, IFontResolver get => !string.IsNullOrWhiteSpace(_workingFolder); } + public bool CanAddItem + { + get => IsTitleBoxVisible && !IsCreatingPDF; + } + public bool IsCreatingPDF { get => _isCreatingPDF; @@ -81,6 +102,7 @@ class MainViewModel : BaseViewModel, IFontResolver NotifyPropertyChanged(); NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF)); + NotifyPropertyChanged(nameof(CanAddItem)); } } @@ -117,6 +139,16 @@ class MainViewModel : BaseViewModel, IFontResolver set { _createPDFLog = value; NotifyPropertyChanged(); } } + public bool HasUnsavedWork + { + get => _hasUnsavedWork; + set + { + _hasUnsavedWork = value; + NotifyPropertyChanged(); + } + } + public ObservableCollection ReportFiles { get => _reportFiles; @@ -157,6 +189,7 @@ class MainViewModel : BaseViewModel, IFontResolver _settings.LastUsedPath = folder.Path.LocalPath; await _settings.SaveSettingsAsync(); ResortPDFItemsByDate(); + HasUnsavedWork = true; } } } @@ -167,6 +200,7 @@ class MainViewModel : BaseViewModel, IFontResolver { WorkingFolder = path; NotifyPropertyChanged(nameof(IsTitleBoxVisible)); + NotifyPropertyChanged(nameof(CanAddItem)); var reportFilePath = Path.Combine(path, GetReportSavedDataFileName()); var successfullyLoadedPriorReport = false; if (File.Exists(reportFilePath)) @@ -195,7 +229,11 @@ class MainViewModel : BaseViewModel, IFontResolver { AddFileBasedOnPath(filePath); } - ResortPDFItemsByDate(); + if (!_isPerformingInitialLoad) + { + ResortPDFItemsByDate(); + } + HasUnsavedWork = true; } } else @@ -221,6 +259,7 @@ class MainViewModel : BaseViewModel, IFontResolver if (idx != -1) { ReportFiles.RemoveAt(idx); + HasUnsavedWork = true; } } } @@ -236,18 +275,20 @@ class MainViewModel : BaseViewModel, IFontResolver file.Title = updatedData.Title; file.ReceiptDateTime = updatedData.ReceiptDateTime; file.Notes = updatedData.Notes; + HasUnsavedWork = true; } } private string[] GetAllowedFileExtensionPatterns() { + // update GetAllowedFileExtensionPatternsWithoutStar if this is edited return [ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp", "*.pdf", "*.heic", ]; } private string[] GetAllowedFileExtensionPatternsWithoutStar() { - var list = GetAllowedFileExtensionPatterns(); - return list.Select(x => x.Replace("*.", "")).ToArray(); + // update GetAllowedFileExtensionPatterns if this is edited + return [ "png", "jpg", "jpeg", "gif", "bmp", "webp", "pdf", "heic", ]; } public async void AddItem() @@ -316,6 +357,7 @@ class MainViewModel : BaseViewModel, IFontResolver Notes = "", FilePath = filePath, }); + HasUnsavedWork = true; } } } @@ -351,6 +393,7 @@ class MainViewModel : BaseViewModel, IFontResolver { var file = files[0]; reportFile.FilePath = file.Path.LocalPath; + HasUnsavedWork = true; } } } @@ -389,6 +432,7 @@ class MainViewModel : BaseViewModel, IFontResolver { LogInfo("Sorting report files list..."); ReportFiles = new ObservableCollection(ReportFiles.OrderBy(x => x.ReceiptDateTime)); + HasUnsavedWork = true; } public async void BuildPDF() @@ -423,7 +467,7 @@ class MainViewModel : BaseViewModel, IFontResolver } } - public async void SaveInterimReportInfo() + public async Task SaveInterimReportInfo() { var report = new PDFReport() { @@ -447,6 +491,7 @@ class MainViewModel : BaseViewModel, IFontResolver var savePath = Path.Combine(_workingFolder, GetReportSavedDataFileName()); await File.WriteAllTextAsync(savePath, json); LogInfo("Saved report information to {0}", savePath); + HasUnsavedWork = false; } private async Task CreateAndSaveReportObjectAfterReportCreation() @@ -691,4 +736,33 @@ class MainViewModel : BaseViewModel, IFontResolver OpenFolderForFileInFileViewer(outputPDFFileName); IsCreatingPDF = false; } + + public async Task CheckIsSafeToShutdown() + { + if (!HasUnsavedWork || string.IsNullOrWhiteSpace(WorkingFolder)) + { + return true; + } + else + { + var result = await DialogHost.Show(new ShutdownCheckViewModel()); + if (result != null && result is ShutdownCheckOptions opt) + { + if (opt == ShutdownCheckOptions.SaveAndShutdown) + { + await SaveInterimReportInfo(); + return true; + } + else if (opt == ShutdownCheckOptions.NoSaveShutdown) + { + return true; + } + else if (opt == ShutdownCheckOptions.CancelShutdown) + { + return false; + } + } + } + return false; + } } \ No newline at end of file diff --git a/src/ViewModels/ShutdownCheckViewModel.cs b/src/ViewModels/ShutdownCheckViewModel.cs new file mode 100644 index 0000000..a823ecd --- /dev/null +++ b/src/ViewModels/ShutdownCheckViewModel.cs @@ -0,0 +1,29 @@ +#nullable enable + +using DialogHostAvalonia; +using MayShow.Models; + +namespace MayShow.ViewModels; + +class ShutdownCheckViewModel +{ + + public ShutdownCheckViewModel() + { + } + + public void SaveAndShutdown() + { + DialogHost.Close("DialogHost", ShutdownCheckOptions.SaveAndShutdown); + } + + public void DoNotSaveAndShutdown() + { + DialogHost.Close("DialogHost", ShutdownCheckOptions.NoSaveShutdown); + } + + public void CancelShutdown() + { + DialogHost.Close("DialogHost", ShutdownCheckOptions.CancelShutdown); + } +} \ No newline at end of file diff --git a/src/Views/MainView.axaml b/src/Views/MainView.axaml index 14f4134..e7ad9b5 100644 --- a/src/Views/MainView.axaml +++ b/src/Views/MainView.axaml @@ -223,7 +223,7 @@