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