24 Commits

Author SHA1 Message Date
mbabienco 00d35a5ead Try some more algorithms
We got the black background, receipt foreground working, but this is a limited test case. I do not have something working in the general case nor the expertise to know how to do that.
2026-03-01 15:26:36 +09:00
mbabienco 7b854d7216 Sort of finds the receipt not really
Need to read more into params and figure out best config and then run with it
2026-02-27 18:08:53 +09:00
mbabienco af0a5d0501 Add dependency (macOS) on OpenCv 2026-02-27 17:36:28 +09:00
mbabienco c3a4c4ae96 Swap to Magick.NET-Q8 for faster processing 2026-02-27 17:36:15 +09:00
mbabienco bb6cdf0abf Tweak where height/width loaded/set 2026-02-25 09:33:44 +09:00
mbabienco 3c754dc196 Refine log messages 2026-02-25 09:15:02 +09:00
mbabienco af8cfa31e5 Fix comment 2026-02-25 09:14:28 +09:00
mbabienco f32568c918 Downsize if > 1.5 MB 2026-02-25 07:50:28 +09:00
mbabienco b42da7603b Auto-rotate images based on EXIF data 2026-02-25 07:45:51 +09:00
mbabienco 851772398c If file > 2.5 MB reduce size 2026-02-25 07:37:11 +09:00
mbabienco dfc1c557e0 Refactor extensions to Constants class 2026-02-25 07:35:51 +09:00
mbabienco f927667732 Add remove all button 2026-02-25 07:29:43 +09:00
mbabienco d77f9bab8c Don't show file not added for project json file 2026-02-24 21:03:14 +09:00
mbabienco d508222901 Update AppScreenshot.png 2026-02-24 21:02:16 +09:00
mbabienco 45e45d44af Update app.manifest 2026-02-24 19:48:18 +09:00
mbabienco d0d39ccd62 Add note 2026-02-24 19:47:06 +09:00
mbabienco ff96d727f1 Fix width on edit page 2026-02-24 19:44:58 +09:00
mbabienco 828430adf8 Fix sort not affecting saved status 2026-02-24 19:43:36 +09:00
mbabienco a6b60a4cc8 Bump app version 2026-02-24 19:40:43 +09:00
mbabienco 2fb0e1f73b Adjust max width in about screen 2026-02-24 19:39:11 +09:00
mbabienco 2a8bbf76bf Warn before software closed; fix bug on first run
Also put settings into proper dir
2026-02-24 19:38:20 +09:00
mbabienco bc5ce3e311 Don't highlight today in edit screen 2026-02-24 10:33:22 +09:00
mbabienco 64dc54bb70 Fix spacing of console line 2026-02-24 09:23:30 +09:00
mbabienco c6273ed8b3 Fix edit button icon 2026-02-24 09:14:21 +09:00
26 changed files with 690 additions and 96 deletions
+21 -1
View File
@@ -1,3 +1,22 @@
-Add ImageMagick attribution to about page
-auto detect receipts in an image and auto-crop?
-https://imagemagick.org/api/feature.php#gsc.tab=0 canny edge image
-https://blog.jiayu.co/2019/05/edge-detection-with-imagemagick/
-https://pyimagesearch.com/2021/10/27/automatically-ocring-receipts-and-scans/ using open CV
-https://www.kaggle.com/code/dmitryyemelyanov/receipt-ocr-part-1-image-segmentation-by-opencv manip done before edge detect
-https://www.luisllamas.es/en/how-to-use-opencv-in-net-with-opencvsharp/ (some basic code but also line detect)
-opencv for macOS? https://www.nuget.org/packages/OpenCvSharp4.runtime.osx.10.15-universal
macOS arm64: https://www.nuget.org/packages/OpenCvSharp4.runtime.osx_arm64/4.8.1-rc
-can use the normal nuget for windows, linnux
-if we can get openCV working then we can probably hack something together...
-https://github.com/shimat/opencvsharp/issues/949 -- requires ffmpeg?!
-https://github.com/shimat/opencvsharp
-https://www.emgu.com/wiki/index.php?title=Main_Page (GPL...)
-https://stackoverflow.com/questions/30296710/detecting-paper-edge-and-crop-it
//https://developers.goalist.co.jp/entry/2019/02/13/150126
---------------
*-add more items *-add more items
*-save last opened folder to settings somewhere *-save last opened folder to settings somewhere
@@ -21,4 +40,5 @@
---Publishing--- ---Publishing---
*-Published app has unneeded .DSYM file (fixed via .app builder) *-Published app has unneeded .DSYM file (fixed via .app builder)
*-Published app has Assets folder already copied to it; don't want that in output macOS folder but it's being copied there anyway (fixed via .app builder) *-Published app has Assets folder already copied to it; don't want that in output macOS folder but it's being copied there anyway (fixed via .app builder)
*-macOS x64 build *-macOS x64 build
Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 236 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
; Non-commercial use only ; Non-commercial use only
#define MyAppName "MayShow" #define MyAppName "MayShow"
#define MyAppVersion "1.1.0" #define MyAppVersion "1.2.0"
#define MyAppPublisher "Quickity Quack Productions" #define MyAppPublisher "Quickity Quack Productions"
#define MyAppExeName "MayShow.exe" #define MyAppExeName "MayShow.exe"
+6
View File
@@ -100,6 +100,12 @@
<DataTemplate DataType="{x:Type viewModels:WarningViewModel}"> <DataTemplate DataType="{x:Type viewModels:WarningViewModel}">
<views:WarningView/> <views:WarningView/>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ShutdownCheckViewModel}">
<views:ShutdownCheckView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ConfirmViewModel}">
<views:ConfirmView/>
</DataTemplate>
</Application.DataTemplates> </Application.DataTemplates>
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
+1 -1
View File
@@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AvaloniaVersion>12.0.0-preview1</AvaloniaVersion> <AvaloniaVersion>11.3.12</AvaloniaVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+4 -1
View File
@@ -5,7 +5,10 @@ namespace MayShow.Helpers;
class Constants class Constants
{ {
public static string AppVersion = "1.1.0"; public static string AppVersion = "1.2.0";
public static string[] AllowedFileExtensionPatterns = [ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp", "*.pdf", "*.heic", ];
public static string[] AllowedFileExtensionsNoStar = [ "png", "jpg", "jpeg", "gif", "bmp", "webp", "pdf", "heic", ];
public static string[] GetQuotes() public static string[] GetQuotes()
{ {
+8
View File
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace MayShow.Interfaces;
interface ICanCheckShutdown
{
Task<bool> CheckIsSafeToShutdown();
}
+5 -6
View File
@@ -1,10 +1,9 @@
using MayShow.ViewModels; 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();
}
} }
+4 -5
View File
@@ -1,9 +1,8 @@
using Avalonia.Controls; using Avalonia.Controls;
namespace MayShow.Interfaces namespace MayShow.Interfaces;
interface ITopLevelGrabber
{ {
interface ITopLevelGrabber TopLevel GetTopLevel();
{
TopLevel GetTopLevel();
}
} }
+2 -1
View File
@@ -13,7 +13,8 @@
Height="650" Height="650"
MinHeight="550"> MinHeight="550">
<dialogHost:DialogHost CloseOnClickAway="False" <dialogHost:DialogHost CloseOnClickAway="False"
Identifier="DialogHost"> Identifier="DialogHost"
x:Name="WindowDialogHost">
<dialogHost:DialogHost.DialogContent> <dialogHost:DialogHost.DialogContent>
<StackPanel/> <StackPanel/>
</dialogHost:DialogHost.DialogContent> </dialogHost:DialogHost.DialogContent>
+59
View File
@@ -1,4 +1,9 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using DialogHostAvalonia;
using MayShow.Interfaces; using MayShow.Interfaces;
using MayShow.ViewModels; using MayShow.ViewModels;
@@ -10,6 +15,60 @@ public partial class MainWindow : Window, ITopLevelGrabber
{ {
InitializeComponent(); InitializeComponent();
DataContext = new MainWindowViewModel(this); 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<bool> 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() public TopLevel GetTopLevel()
+12 -4
View File
@@ -12,7 +12,7 @@
<PublishTrimmed>true</PublishTrimmed> <PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<AssemblyName>MayShow</AssemblyName> <AssemblyName>MayShow</AssemblyName>
<AssemblyVersion>1.1.0</AssemblyVersion> <!-- Also update Constants version --> <AssemblyVersion>1.2.0</AssemblyVersion> <!-- Also update Constants version -->
<ApplicationIcon>MayShow-icon.ico</ApplicationIcon> <ApplicationIcon>MayShow-icon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -42,10 +42,18 @@
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.3" /> <PackageReference Include="PDFsharp-MigraDoc" Version="6.2.3" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.10.2" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.3" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" Version="0.11.0-preview20260220" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing" Version="0.10.11-preview20251127001" />
<PackageReference Include="DialogHost.Avalonia" Version="0.10.4-avalonia12" /> <PackageReference Include="DialogHost.Avalonia" Version="0.10.4" />
<PackageReference Include="Xaml.Behaviors.Interactions.DragAndDrop.DataGrid" Version="11.3.9.5" /> <PackageReference Include="Xaml.Behaviors.Interactions.DragAndDrop.DataGrid" Version="11.3.9.5" />
<PackageReference Include="OpenCvSharp4" Version="4.13.0.20260226" />
</ItemGroup>
<ItemGroup Condition=" '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' ">
<PackageReference Include="OpenCvSharp4.runtime.osx.10.15-universal" Version="4.7.0.20230224" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -1
View File
@@ -34,7 +34,7 @@ class Settings : ChangeNotifier
{ {
var path = Path.Combine( var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ReceiptPDFBuilder" // legacy name for existing settings prior to app name change "MayShow"
); );
if (!Directory.Exists(path)) if (!Directory.Exists(path))
{ {
+8
View File
@@ -0,0 +1,8 @@
namespace MayShow.Models;
enum ShutdownCheckOptions
{
SaveAndShutdown,
NoSaveShutdown,
CancelShutdown,
}
+52
View File
@@ -0,0 +1,52 @@
#nullable enable
using DialogHostAvalonia;
using MayShow.Helpers;
namespace MayShow.ViewModels;
class ConfirmViewModel
{
private string _title;
private string _message;
private string _confirmTitle;
private string _declineTitle;
public ConfirmViewModel(string title, string message, string confirmTitle = "Yes", string declineTitle = "No")
{
_title = title;
_message = message;
_confirmTitle = confirmTitle;
_declineTitle = declineTitle;
}
public string Title
{
get => _title;
}
public string Message
{
get => _message;
}
public string ConfirmTitle
{
get => _confirmTitle;
}
public string DeclineTitle
{
get => _declineTitle;
}
public void Confirm()
{
DialogHost.Close("DialogHost", true);
}
public void Decline()
{
DialogHost.Close("DialogHost", false);
}
}
+334 -58
View File
@@ -20,11 +20,16 @@ using MayShow.Helpers;
using MayShow.Interfaces; using MayShow.Interfaces;
using MayShow.Models; using MayShow.Models;
using MayShows.Helpers; using MayShows.Helpers;
using OpenCvSharp;
using System.Reflection.Metadata.Ecma335;
using System.Collections.Generic;
using System.Threading;
namespace MayShow.ViewModels; namespace MayShow.ViewModels;
class MainViewModel : BaseViewModel, IFontResolver class MainViewModel : BaseViewModel, IFontResolver, ICanCheckShutdown
{ {
private bool _isPerformingInitialLoad;
private string _processDir; private string _processDir;
private bool _isCreatingPDF; private bool _isCreatingPDF;
private string _createPDFLog; private string _createPDFLog;
@@ -36,21 +41,24 @@ class MainViewModel : BaseViewModel, IFontResolver
private Settings _settings; private Settings _settings;
private bool _hasUnsavedWork;
public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger) public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
{ {
_isPerformingInitialLoad = true;
_processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? ""; _processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
Console.WriteLine("Process is running from: {0}", _processDir); Console.WriteLine("Process is running from: {0}", _processDir);
_isCreatingPDF = false; _isCreatingPDF = false;
var quotes = Constants.GetQuotes(); var quotes = Constants.GetQuotes();
Random random = new Random(); Random random = new Random();
var quoteIndex = random.Next(0, quotes.Length); var quoteIndex = random.Next(0, quotes.Length);
_createPDFLog = "----- MayShow v" + Constants.AppVersion + "------" + Environment.NewLine; _createPDFLog = "----- MayShow v" + Constants.AppVersion + " ------" + Environment.NewLine;
_createPDFLog += quotes[quoteIndex] + Environment.NewLine; _createPDFLog += quotes[quoteIndex] + Environment.NewLine;
_createPDFLog += "---------------------------------------" + Environment.NewLine; _createPDFLog += "---------------------------------------" + Environment.NewLine;
_createPDFLog += "Ready to create PDF!"; _createPDFLog += "Loaded and ready to create report!" + Environment.NewLine;
_createPDFLog += "Please copy and send this Program Log when reporting any issues with the software.";
_workingFolder = ""; _workingFolder = "";
_reportFiles = new ObservableCollection<ReportFile>(); ReportFiles = _reportFiles = new ObservableCollection<ReportFile>();
_reportFiles.CollectionChanged += ( sender, e ) => { NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); };
_reportTitle = ""; _reportTitle = "";
_lastGeneratedTime = null; _lastGeneratedTime = null;
_settings = Settings.LoadSettings(); _settings = Settings.LoadSettings();
@@ -59,12 +67,24 @@ class MainViewModel : BaseViewModel, IFontResolver
LogInfo("Loading data at last used path of {0}", _settings.LastUsedPath); LogInfo("Loading data at last used path of {0}", _settings.LastUsedPath);
ScanFolder(_settings.LastUsedPath); ScanFolder(_settings.LastUsedPath);
} }
else
{
LogInfo("Choose a receipt folder to begin...");
}
HasUnsavedWork = false;
_isPerformingInitialLoad = false;
} }
public string ReportTitle public string ReportTitle
{ {
get => _reportTitle; get => _reportTitle;
set { _reportTitle = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(IsTitleBoxVisible)); } set
{
_reportTitle = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsTitleBoxVisible));
NotifyPropertyChanged(nameof(CanAddItem));
}
} }
public bool IsTitleBoxVisible public bool IsTitleBoxVisible
@@ -72,6 +92,11 @@ class MainViewModel : BaseViewModel, IFontResolver
get => !string.IsNullOrWhiteSpace(_workingFolder); get => !string.IsNullOrWhiteSpace(_workingFolder);
} }
public bool CanAddItem
{
get => IsTitleBoxVisible && !IsCreatingPDF;
}
public bool IsCreatingPDF public bool IsCreatingPDF
{ {
get => _isCreatingPDF; get => _isCreatingPDF;
@@ -81,6 +106,7 @@ class MainViewModel : BaseViewModel, IFontResolver
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF)); NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF));
NotifyPropertyChanged(nameof(CanAddItem));
} }
} }
@@ -117,6 +143,16 @@ class MainViewModel : BaseViewModel, IFontResolver
set { _createPDFLog = value; NotifyPropertyChanged(); } set { _createPDFLog = value; NotifyPropertyChanged(); }
} }
public bool HasUnsavedWork
{
get => _hasUnsavedWork;
set
{
_hasUnsavedWork = value;
NotifyPropertyChanged();
}
}
public ObservableCollection<ReportFile> ReportFiles public ObservableCollection<ReportFile> ReportFiles
{ {
get => _reportFiles; get => _reportFiles;
@@ -126,7 +162,8 @@ class MainViewModel : BaseViewModel, IFontResolver
NotifyPropertyChanged(); NotifyPropertyChanged();
_reportFiles.CollectionChanged += ( sender, e ) => _reportFiles.CollectionChanged += ( sender, e ) =>
{ {
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
HasUnsavedWork = true;
}; };
} }
} }
@@ -157,6 +194,7 @@ class MainViewModel : BaseViewModel, IFontResolver
_settings.LastUsedPath = folder.Path.LocalPath; _settings.LastUsedPath = folder.Path.LocalPath;
await _settings.SaveSettingsAsync(); await _settings.SaveSettingsAsync();
ResortPDFItemsByDate(); ResortPDFItemsByDate();
HasUnsavedWork = true;
} }
} }
} }
@@ -167,6 +205,7 @@ class MainViewModel : BaseViewModel, IFontResolver
{ {
WorkingFolder = path; WorkingFolder = path;
NotifyPropertyChanged(nameof(IsTitleBoxVisible)); NotifyPropertyChanged(nameof(IsTitleBoxVisible));
NotifyPropertyChanged(nameof(CanAddItem));
var reportFilePath = Path.Combine(path, GetReportSavedDataFileName()); var reportFilePath = Path.Combine(path, GetReportSavedDataFileName());
var successfullyLoadedPriorReport = false; var successfullyLoadedPriorReport = false;
if (File.Exists(reportFilePath)) if (File.Exists(reportFilePath))
@@ -195,7 +234,11 @@ class MainViewModel : BaseViewModel, IFontResolver
{ {
AddFileBasedOnPath(filePath); AddFileBasedOnPath(filePath);
} }
ResortPDFItemsByDate(); if (!_isPerformingInitialLoad)
{
ResortPDFItemsByDate();
}
HasUnsavedWork = true;
} }
} }
else else
@@ -221,6 +264,7 @@ class MainViewModel : BaseViewModel, IFontResolver
if (idx != -1) if (idx != -1)
{ {
ReportFiles.RemoveAt(idx); ReportFiles.RemoveAt(idx);
HasUnsavedWork = true;
} }
} }
} }
@@ -236,20 +280,10 @@ class MainViewModel : BaseViewModel, IFontResolver
file.Title = updatedData.Title; file.Title = updatedData.Title;
file.ReceiptDateTime = updatedData.ReceiptDateTime; file.ReceiptDateTime = updatedData.ReceiptDateTime;
file.Notes = updatedData.Notes; file.Notes = updatedData.Notes;
HasUnsavedWork = true;
} }
} }
private string[] GetAllowedFileExtensionPatterns()
{
return [ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp", "*.pdf", "*.heic", ];
}
private string[] GetAllowedFileExtensionPatternsWithoutStar()
{
var list = GetAllowedFileExtensionPatterns();
return list.Select(x => x.Replace("*.", "")).ToArray();
}
public async void AddItem() public async void AddItem()
{ {
var topLevel = TopLevelGrabber?.GetTopLevel(); var topLevel = TopLevelGrabber?.GetTopLevel();
@@ -262,7 +296,7 @@ class MainViewModel : BaseViewModel, IFontResolver
FileTypeFilter = [ FileTypeFilter = [
new FilePickerFileType("All Types") new FilePickerFileType("All Types")
{ {
Patterns = GetAllowedFileExtensionPatterns(), Patterns = Constants.AllowedFileExtensionPatterns,
AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ], AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
MimeTypes = [ "image/*", "application/pdf", "image/heic" ] MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
}, },
@@ -292,7 +326,7 @@ class MainViewModel : BaseViewModel, IFontResolver
if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath) && !filePath.EndsWith(".DS_Store")) if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath) && !filePath.EndsWith(".DS_Store"))
{ {
// make sure extensions are OK // make sure extensions are OK
var fileExtensions = GetAllowedFileExtensionPatternsWithoutStar(); var fileExtensions = Constants.AllowedFileExtensionsNoStar;
var didMatch = false; var didMatch = false;
foreach (var fileExtension in fileExtensions) foreach (var fileExtension in fileExtensions)
{ {
@@ -304,7 +338,10 @@ class MainViewModel : BaseViewModel, IFontResolver
} }
if (!didMatch) if (!didMatch)
{ {
LogInfo("File {0} did not match allowed file extension types, so it was not added.", filePath); if (!filePath.EndsWith(GetReportSavedDataFileName()))
{
LogInfo("File {0} did not match allowed file extension types, so it was not added.", filePath);
}
} }
else else
{ {
@@ -316,10 +353,22 @@ class MainViewModel : BaseViewModel, IFontResolver
Notes = "", Notes = "",
FilePath = filePath, FilePath = filePath,
}); });
HasUnsavedWork = true;
} }
} }
} }
public async void RemoveAllItems()
{
var result = await DialogHost.Show(new ConfirmViewModel("Warning!", "Are you sure you want to remove all items from this report?", "Remove All Items", "Cancel"));
if (result != null && (bool)result)
{
ReportFiles.Clear();
HasUnsavedWork = true;
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
}
}
public void LocateFile(object f) => LocateFileImpl((ReportFile) f); public void LocateFile(object f) => LocateFileImpl((ReportFile) f);
public async void LocateFileImpl(ReportFile reportFile) public async void LocateFileImpl(ReportFile reportFile)
{ {
@@ -333,7 +382,7 @@ class MainViewModel : BaseViewModel, IFontResolver
FileTypeFilter = [ FileTypeFilter = [
new FilePickerFileType("All Types") new FilePickerFileType("All Types")
{ {
Patterns = GetAllowedFileExtensionPatterns(), Patterns = Constants.AllowedFileExtensionPatterns,
AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ], AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
MimeTypes = [ "image/*", "application/pdf", "image/heic" ] MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
}, },
@@ -351,6 +400,7 @@ class MainViewModel : BaseViewModel, IFontResolver
{ {
var file = files[0]; var file = files[0];
reportFile.FilePath = file.Path.LocalPath; reportFile.FilePath = file.Path.LocalPath;
HasUnsavedWork = true;
} }
} }
} }
@@ -389,6 +439,7 @@ class MainViewModel : BaseViewModel, IFontResolver
{ {
LogInfo("Sorting report files list..."); LogInfo("Sorting report files list...");
ReportFiles = new ObservableCollection<ReportFile>(ReportFiles.OrderBy(x => x.ReceiptDateTime)); ReportFiles = new ObservableCollection<ReportFile>(ReportFiles.OrderBy(x => x.ReceiptDateTime));
HasUnsavedWork = true;
} }
public async void BuildPDF() public async void BuildPDF()
@@ -423,7 +474,7 @@ class MainViewModel : BaseViewModel, IFontResolver
} }
} }
public async void SaveInterimReportInfo() public async Task SaveInterimReportInfo()
{ {
var report = new PDFReport() var report = new PDFReport()
{ {
@@ -447,6 +498,7 @@ class MainViewModel : BaseViewModel, IFontResolver
var savePath = Path.Combine(_workingFolder, GetReportSavedDataFileName()); var savePath = Path.Combine(_workingFolder, GetReportSavedDataFileName());
await File.WriteAllTextAsync(savePath, json); await File.WriteAllTextAsync(savePath, json);
LogInfo("Saved report information to {0}", savePath); LogInfo("Saved report information to {0}", savePath);
HasUnsavedWork = false;
} }
private async Task CreateAndSaveReportObjectAfterReportCreation() private async Task CreateAndSaveReportObjectAfterReportCreation()
@@ -468,6 +520,190 @@ class MainViewModel : BaseViewModel, IFontResolver
return "report_data.json"; return "report_data.json";
} }
public void TestReceiptFinding(object f) => TestReceiptFindingImpl((ReportFile)f);
private Mat? b_algor(ReportFile file)
{
using var orig = new Mat(file.FilePath);
using var src = new Mat(file.FilePath, ImreadModes.Grayscale);
if (src.Empty())
{
LogInfo("File was empty?");
return null;
}
using var blur = orig.GaussianBlur(new Size(5, 5), 0.0);
using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(9,9));
using var dilated = blur.Dilate(kernel, anchor: null, iterations: 4);
using var edges = dilated.Canny(100, 200, 3);
using var heirarchy = new Mat();
Cv2.FindContours(edges, out Mat[] contours, heirarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
using var orgWithContours = new Mat();
orig.CopyTo(orgWithContours);
Cv2.DrawContours(orgWithContours, contours, -1, Scalar.Cyan, 3);
// largest contours
var largestContours = contours.OrderByDescending(x => x.ContourArea()).Take(1).ToArray();
var orgWithLargestContours = new Mat();
orig.CopyTo(orgWithLargestContours);
Cv2.DrawContours(orgWithLargestContours, largestContours, -1, Scalar.Cyan, 5);
using (new OpenCvSharp.Window("blur", blur))
using (new OpenCvSharp.Window("dilated", dilated))
using (new OpenCvSharp.Window("edges", edges))
using (new OpenCvSharp.Window("contours", orgWithContours))
// using (new OpenCvSharp.Window("w biggest contours", orgWithBiggestContours))
{
Cv2.WaitKey();
}
return orgWithLargestContours;
}
private void TestReceiptFindingImpl(ReportFile file)
{
LogInfo("Running receipt edge detection on file at path {0} with OpenCV {1}...", file.FilePath, Cv2.GetVersionString() ?? "");
using var orig = new Mat(file.FilePath);
using var src = new Mat(file.FilePath, ImreadModes.Grayscale);
using var dst = new Mat();
using var wContours = new Mat();
if (src.Empty())
{
LogInfo("File was empty?");
return;
}
var threshold = src.Threshold(90,255, ThresholdTypes.Binary);
var blur = orig.GaussianBlur(new Size(3.0, 3.0), 0.0);
var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(9,9));
var dilated = blur.Dilate(kernel, null, 1);
var edges = dilated.Canny(50, 200, 3);
//# Detect all contours in Canny-edged image
using var heirarchy = new Mat();
Cv2.FindContours(threshold, out Mat[] contours, heirarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
var orgWithContours = new Mat();
orig.CopyTo(orgWithContours);
Cv2.DrawContours(orgWithContours, contours, -1, Scalar.Cyan, 3);
// # find full contours
var poly = new List<Mat>();
foreach (var contour in contours)
{
var hull = contour.ConvexHull();
poly.Add(hull.ApproxPolyDP(0.01 * Cv2.ArcLength(hull, true), false));
}
Console.WriteLine("How many? {0}", poly.Count);
var orgWithAllPly = new Mat();
orig.CopyTo(orgWithAllPly);
Cv2.DrawContours(orgWithAllPly, poly, -1, Scalar.Red, 3);
var largestPoly = poly.OrderByDescending(x => x.ContourArea()).Take(10).ToArray();
var orgWithLargestContoursPly = new Mat();
orig.CopyTo(orgWithLargestContoursPly);
Cv2.DrawContours(orgWithLargestContoursPly, largestPoly, -1, Scalar.Red, 5);
// using (new OpenCvSharp.Window("poly", orgWithAllPly))
// using (new OpenCvSharp.Window("pol2y", orgWithLargestContoursPly))
// {
// Cv2.WaitKey();
// }
//
// # Get 10 largest contours
Console.WriteLine(contours);
var orgWithLargestContours = new Mat();
orig.CopyTo(orgWithLargestContours);
var largestContours = contours.OrderByDescending(x => x.ContourArea()).Take(10).ToArray();
Cv2.DrawContours(orgWithLargestContours, largestContours, -1, Scalar.Cyan, 5);
//
Mat approximate_contour(Mat contour)
{
var perimeter = Cv2.ArcLength(contour, true);
return contour.ApproxPolyDP(0.02 * perimeter, true);
}
Mat? get_receipt_counter(Mat[] contours)
{
var poly = new List<Mat>();
foreach (var contour in contours)
{
// var hull = contour.ConvexHull();
// var arcLength = hull.ArcLength(true);
// var x = contour.ApproxPolyDP(0.01 * arcLength, true);
// poly.Add(x);
var approx = approximate_contour(contour);
if (approx.Total() == 4)
{
return approx;
}
}
return null;
}
var highestY = 0;
var lowestY = 9999;
var highestX = 0;
var lowestX = 9999;
foreach (var contour in largestContours)
{
Console.WriteLine("Rows: {0}", contour.Rows);
if (contour.Rows > 10) // eliminate small things?
{
for (var i = 0; i < contour.Rows; i++)
{
var pt = contour.At<Point>(i);
if (pt.X < lowestX)
{
lowestX = pt.X;
}
if (pt.X > highestX)
{
highestX = pt.X;
}
if (pt.Y < lowestY)
{
lowestY = pt.Y;
}
if (pt.Y > highestY)
{
highestY = pt.Y;
}
}
}
}
Console.WriteLine("Low X: {0}, High X: {1}", lowestX, highestX);
Console.WriteLine("Low Y: {0}, High Y: {1}", lowestY, highestY);
var rect = new Rect(lowestX, lowestY, Math.Abs(highestX - lowestX), Math.Abs(highestY - lowestY));
Console.WriteLine(rect);
using var crop = new Mat(orig, rect);
using (new OpenCvSharp.Window("w largest contours", orgWithLargestContours))
using (new OpenCvSharp.Window("crop", crop))
using (new OpenCvSharp.Window("crop_b", b_algor(file)))
// using (new OpenCvSharp.Window("w biggest contours", orgWithBiggestContours))
{
Cv2.WaitKey();
}
return;
var largest = get_receipt_counter(largestContours);
Console.WriteLine(largest);
var orgWithBiggestContours = new Mat();
orig.CopyTo(orgWithBiggestContours);
Cv2.DrawContours(orgWithBiggestContours, [largest], -1, Scalar.Cyan, 3);
////
using (new OpenCvSharp.Window("src image", src))
// using (new OpenCvSharp.Window("blur image", blur))
// using (new OpenCvSharp.Window("dilated image", dilated))
using (new OpenCvSharp.Window("orig with all contours", orgWithContours))
using (new OpenCvSharp.Window("w largest contours", orgWithLargestContours))
// using (new OpenCvSharp.Window("w biggest contours", orgWithBiggestContours))
{
Cv2.WaitKey();
}
}
public byte[]? GetFont(string faceName) public byte[]? GetFont(string faceName)
{ {
LogInfo(string.Format("Loading font {0}", faceName)); LogInfo(string.Format("Loading font {0}", faceName));
@@ -558,6 +794,12 @@ class MainViewModel : BaseViewModel, IFontResolver
reportTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too reportTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
reportTitlePar.AddText(ReportTitle); reportTitlePar.AddText(ReportTitle);
// //
var convertedDir = Path.Combine(folderPath, "converted");
if (!Directory.Exists(convertedDir))
{
Directory.CreateDirectory(convertedDir);
}
//
GlobalFontSettings.FontResolver = this; GlobalFontSettings.FontResolver = this;
GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver(); GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
var hasAddedData = false; var hasAddedData = false;
@@ -585,14 +827,7 @@ class MainViewModel : BaseViewModel, IFontResolver
imageTitlePar.Format.Font.Size = 12; imageTitlePar.Format.Font.Size = 12;
imageTitlePar.Format.Font.Bold = true; imageTitlePar.Format.Font.Bold = true;
imageTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too imageTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
if (string.IsNullOrWhiteSpace(file.Title)) imageTitlePar.AddText(string.IsNullOrWhiteSpace(file.Title) ? file.FileName : file.Title);
{
imageTitlePar.AddText(file.FileName);
}
else
{
imageTitlePar.AddText(file.Title);
}
var receiptDatePar = section.AddParagraph(); var receiptDatePar = section.AddParagraph();
receiptDatePar.Format.Alignment = ParagraphAlignment.Center; receiptDatePar.Format.Alignment = ParagraphAlignment.Center;
receiptDatePar.Format.Font.Size = 12; receiptDatePar.Format.Font.Size = 12;
@@ -620,35 +855,47 @@ class MainViewModel : BaseViewModel, IFontResolver
var info = new FileInfo(file.FilePath); var info = new FileInfo(file.FilePath);
uint loadedImageWidth = 0; uint loadedImageWidth = 0;
uint loadedImageHeight = 0; uint loadedImageHeight = 0;
if (isHEIC || isWebp || isPNG) if (!isPDF)
{ {
// Save image as jpg
var convertedDir = Path.Combine(folderPath, "converted");
if (!Directory.Exists(convertedDir))
{
Directory.CreateDirectory(convertedDir);
}
var outputPath = Path.Combine(convertedDir, info.Name + ".jpg");
using var mImage = new MagickImage(info.FullName); using var mImage = new MagickImage(info.FullName);
loadedImageWidth = mImage.Width; loadedImageWidth = mImage.Width;
loadedImageHeight = mImage.Height; loadedImageHeight = mImage.Height;
mImage.Quality = 80; var convertedOutputPath = Path.Combine(convertedDir, info.Name + ".jpg");
if (mImage.Width >= 400 || mImage.Height >= 400) var didAdjust = false;
LogInfo("Image orientation of {0} is {1}", fileName, mImage.Orientation);
if (mImage.Orientation != OrientationType.TopLeft)
{ {
loadedImageWidth = (uint)Math.Floor(mImage.Width * 0.5); LogInfo("Auto-adjusted image orientation of {0}", fileName);
loadedImageHeight = (uint)Math.Floor(mImage.Height * 0.5); mImage.AutoOrient();
mImage.Scale(loadedImageWidth, loadedImageHeight); didAdjust = true;
}
// perform needed image manipulations
if (isHEIC || isWebp || isPNG || (!isPDF && info.Length > 1.5 * 1024 * 1024 /* 1.5 MB */))
{
// Save image as jpg
mImage.Quality = 80;
if (mImage.Width >= 400 || mImage.Height >= 400)
{
loadedImageWidth = (uint)Math.Floor(mImage.Width * 0.5);
loadedImageHeight = (uint)Math.Floor(mImage.Height * 0.5);
mImage.Scale(loadedImageWidth, loadedImageHeight);
LogInfo("Image {2} scaled to {0}x{1}", loadedImageWidth, loadedImageHeight, fileName);
}
didAdjust = true;
LogInfo("Converted image {0} to JPEG", fileName);
}
else
{
// load height/width
loadedImageWidth = mImage.Width;
loadedImageHeight = mImage.Height;
}
if (didAdjust)
{
await mImage.WriteAsync(convertedOutputPath);
filePath = Path.Combine("Converted", info.Name + ".jpg");
LogInfo(string.Format("Saved adjusted image to JPEG; fileName is now {0}", file.FilePath));
} }
await mImage.WriteAsync(outputPath);
filePath = Path.Combine("Converted", info.Name + ".jpg");
LogInfo(string.Format("Converted image to JPEG; fileName is now {0}", file.FilePath));
}
else if (!isPDF)
{
// load height/width
using var mImage = new MagickImage(info.FullName);
loadedImageWidth = mImage.Width;
loadedImageHeight = mImage.Height;
} }
var paragraph = section.AddParagraph(); var paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center; paragraph.Format.Alignment = ParagraphAlignment.Center;
@@ -688,14 +935,43 @@ class MainViewModel : BaseViewModel, IFontResolver
Document = pdfDoc, Document = pdfDoc,
WorkingDirectory = folderPath WorkingDirectory = folderPath
}; };
LogInfo("Rendering document..."); LogInfo("Rendering document to PDF file...");
pdfRenderer.RenderDocument(); pdfRenderer.RenderDocument();
string outputPDFFileName = Path.Join(folderPath, outputFileName); string outputPDFFileName = Path.Join(folderPath, outputFileName);
LogInfo("Saving document to disk..."); LogInfo("Saving PDF document to disk...");
pdfRenderer.PdfDocument.Save(outputPDFFileName); pdfRenderer.PdfDocument.Save(outputPDFFileName);
LogInfo("Saved PDF output to: " + outputPDFFileName); LogInfo("Finished saving PDF output to: " + outputPDFFileName);
await CreateAndSaveReportObjectAfterReportCreation(); await CreateAndSaveReportObjectAfterReportCreation();
OpenFolderForFileInFileViewer(outputPDFFileName); OpenFolderForFileInFileViewer(outputPDFFileName);
IsCreatingPDF = false; IsCreatingPDF = false;
} }
public async Task<bool> 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;
}
} }
+29
View File
@@ -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);
}
}
+2 -4
View File
@@ -7,7 +7,8 @@
xmlns:models="clr-namespace:MayShow.Models" xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels" xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:AboutViewModel"> x:DataType="vm:AboutViewModel"
MaxWidth="450">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="4"> Spacing="4">
<TextBlock Text="MayShow" <TextBlock Text="MayShow"
@@ -16,15 +17,12 @@
FontSize="18" FontSize="18"
FontWeight="Bold"/> FontWeight="Bold"/>
<TextBlock Text="MayShow (an intentional misspelling of 明証, pronounced may-shō, a Japanese word meaning proof, evidence, or corroboration) is a PDF report creation tool. It was built by MB for A in 2026. May the quacking of ducks always be in your favor. Thanks for using our software!" <TextBlock Text="MayShow (an intentional misspelling of 明証, pronounced may-shō, a Japanese word meaning proof, evidence, or corroboration) is a PDF report creation tool. It was built by MB for A in 2026. May the quacking of ducks always be in your favor. Thanks for using our software!"
MaxWidth="300"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="14"/> FontSize="14"/>
<TextBlock Text="App icon made using https://gauger.me/fonticon/ with FontAwesome icon 'file-invoice-dollar' and the macOS software Icon Composer." <TextBlock Text="App icon made using https://gauger.me/fonticon/ with FontAwesome icon 'file-invoice-dollar' and the macOS software Icon Composer."
MaxWidth="300"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="14"/> FontSize="14"/>
<TextBlock Text="Copyright 2026 - Quickity Quack Productions" <TextBlock Text="Copyright 2026 - Quickity Quack Productions"
MaxWidth="300"
TextWrapping="Wrap" TextWrapping="Wrap"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Margin="0,4,0,4" Margin="0,4,0,4"
+35
View File
@@ -0,0 +1,35 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MayShow.Views.ConfirmView"
xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels"
x:DataType="vm:ConfirmViewModel">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock TextAlignment="Center"
FontWeight="Bold"
FontSize="18"
Text="{Binding Title}"/>
<TextBlock TextAlignment="Center"
FontWeight="Bold"
TextWrapping="Wrap"
FontSize="14"
MaxWidth="350"
Text="{Binding Message}"/>
<StackPanel Orientation="Horizontal"
Spacing="12"
HorizontalAlignment="Right"
Margin="4">
<Button Command="{Binding Decline}"
Content="{Binding DeclineTitle}"
HorizontalAlignment="Right"/>
<Button Command="{Binding Confirm}"
Classes="accent"
Content="{Binding ConfirmTitle}"
HorizontalAlignment="Right"/>
</StackPanel>
</StackPanel>
</UserControl>
+14
View File
@@ -0,0 +1,14 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MayShow.Views;
public partial class ConfirmView : UserControl
{
public ConfirmView()
{
this.InitializeComponent();
}
}
+4 -2
View File
@@ -9,7 +9,8 @@
xmlns:models="clr-namespace:MayShow.Models" xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels" xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:EditFileViewModel"> x:DataType="vm:EditFileViewModel"
MaxWidth="350">
<ScrollViewer AllowAutoHide="False"> <ScrollViewer AllowAutoHide="False">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="4" Spacing="4"
@@ -33,7 +34,8 @@
<Label Content="Receipt Date" /> <Label Content="Receipt Date" />
<Calendar SelectionMode="SingleDate" <Calendar SelectionMode="SingleDate"
SelectedDate="{Binding ClonedFile.ReceiptDateTime}" SelectedDate="{Binding ClonedFile.ReceiptDateTime}"
DisplayDate="{Binding ClonedFile.ReceiptDateTime}" /> DisplayDate="{Binding ClonedFile.ReceiptDateTime}"
IsTodayHighlighted="False" />
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="12" Spacing="12"
Margin="0,4,0,0" Margin="0,4,0,0"
+20 -10
View File
@@ -167,7 +167,7 @@
Margin="2" Margin="2"
IsEnabled="{Binding !$parent[DataGrid].((vm:MainViewModel)DataContext).IsCreatingPDF}"> IsEnabled="{Binding !$parent[DataGrid].((vm:MainViewModel)DataContext).IsCreatingPDF}">
<Button.Content> <Button.Content>
<TextBlock><Run Text="&#xf1f8;" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock> <TextBlock><Run Text="&#xf044;" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock>
</Button.Content> </Button.Content>
</Button> </Button>
<Button Command="{Binding $parent[DataGrid].((vm:MainViewModel)DataContext).RemoveFile}" <Button Command="{Binding $parent[DataGrid].((vm:MainViewModel)DataContext).RemoveFile}"
@@ -210,6 +210,12 @@
<TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock> <TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock>
</Button.Content> </Button.Content>
</Button> </Button>
<Button Command="{Binding $parent[DataGrid].((vm:MainViewModel)DataContext).TestReceiptFinding}"
CommandParameter="{Binding}">
<Button.Content>
<TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> TEST RECEIPT FIND</TextBlock>
</Button.Content>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
@@ -223,27 +229,31 @@
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="4"> Spacing="4">
<Button Command="{Binding AddItem}" <Button Command="{Binding AddItem}"
IsEnabled="{Binding !IsCreatingPDF}"> IsEnabled="{Binding CanAddItem}">
<TextBlock><Run Text="&#x002b;" FontFamily="{StaticResource FontAwesomeSolid}"/> Add Item(s)</TextBlock> <TextBlock><Run Text="&#x002b;" FontFamily="{StaticResource FontAwesomeSolid}"/> Add Item(s)</TextBlock>
</Button> </Button>
<Button Command="{Binding SaveInterimReportInfo}" <Button Command="{Binding RemoveAllItems}"
IsEnabled="{Binding HasWorkingFolderAndNotMakingPDF}"> IsEnabled="{Binding IsCreatePDFButtonEnabled}"
<TextBlock><Run Text="&#xf0c7;" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock> Classes="Danger">
<TextBlock><Run Text="&#xf1f8;" FontFamily="{StaticResource FontAwesomeSolid}"/> Remove All Items</TextBlock>
</Button> </Button>
<Button Command="{Binding ResortPDFItemsByDate}" <Button Command="{Binding ResortPDFItemsByDate}"
IsEnabled="{Binding IsCreatePDFButtonEnabled}"> IsEnabled="{Binding IsCreatePDFButtonEnabled}">
<TextBlock><Run Text="&#xf162;" FontFamily="{StaticResource FontAwesomeSolid}"/> Re-sort PDF Items</TextBlock> <TextBlock><Run Text="&#xf162;" FontFamily="{StaticResource FontAwesomeSolid}"/> Re-sort PDF Items</TextBlock>
</Button> </Button>
<Button Command="{Binding BuildPDF}" <Button Command="{Binding SaveInterimReportInfo}"
Classes="accent" IsEnabled="{Binding HasWorkingFolderAndNotMakingPDF}">
IsEnabled="{Binding IsCreatePDFButtonEnabled}"> <TextBlock><Run Text="&#xf0c7;" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock>
<TextBlock><Run Text="&#xf1c1;" FontFamily="{StaticResource FontAwesomeSolid}"/> Create Report PDF</TextBlock>
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
IsVisible="{Binding IsCreatingPDF}"
Spacing="6" Spacing="6"
HorizontalAlignment="Center"> HorizontalAlignment="Center">
<Button Command="{Binding BuildPDF}"
Classes="accent"
IsEnabled="{Binding IsCreatePDFButtonEnabled}">
<TextBlock><Run Text="&#xf1c1;" FontFamily="{StaticResource FontAwesomeSolid}"/> Create Report PDF</TextBlock>
</Button>
<Label Content="Creating PDF..." <Label Content="Creating PDF..."
IsVisible="{Binding IsCreatingPDF}" IsVisible="{Binding IsCreatingPDF}"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
+14
View File
@@ -3,6 +3,7 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using MayShow.ViewModels;
namespace MayShow.Views namespace MayShow.Views
{ {
@@ -12,6 +13,7 @@ namespace MayShow.Views
{ {
this.InitializeComponent(); this.InitializeComponent();
LogBlock.PropertyChanged += LogBlock_PropertyChanged; LogBlock.PropertyChanged += LogBlock_PropertyChanged;
FilesGrid.CellEditEnded += FileCellEditEnded;
} }
private void LogBlock_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) private void LogBlock_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@@ -26,6 +28,18 @@ namespace MayShow.Views
{ {
var topLevel = TopLevel.GetTopLevel(this); var topLevel = TopLevel.GetTopLevel(this);
topLevel?.FocusManager?.ClearFocus(); topLevel?.FocusManager?.ClearFocus();
if (DataContext is MainViewModel mvm)
{
mvm?.HasUnsavedWork = true;
}
}
private void FileCellEditEnded(object? sender, DataGridCellEditEndedEventArgs args)
{
if (args.EditAction == DataGridEditAction.Commit && DataContext is MainViewModel mvm)
{
mvm?.HasUnsavedWork = true;
}
} }
} }
} }
+39
View File
@@ -0,0 +1,39 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MayShow.Views.ShutdownCheckView"
xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:ShutdownCheckViewModel">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock TextAlignment="Center"
FontWeight="Bold"
FontSize="18"
Text="Warning: You have unsaved report data!"/>
<TextBlock TextAlignment="Center"
FontWeight="Bold"
TextWrapping="Wrap"
FontSize="14"
Text="Do you want to save your data before the program is closed?"/>
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Command="{Binding SaveAndShutdown}"
Classes="accent"
Content="Save Data and Close"
HorizontalAlignment="Right"
Margin="0,4,0,4"/>
<Button Command="{Binding DoNotSaveAndShutdown}"
Content="Do NOT Save Data and Close"
HorizontalAlignment="Right"
Margin="0,4,0,4"/>
<Button Command="{Binding CancelShutdown}"
Content="Cancel Program Shutdown"
HorizontalAlignment="Right"
Margin="0,4,0,4"/>
</StackPanel>
</StackPanel>
</UserControl>
+14
View File
@@ -0,0 +1,14 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MayShow.Views;
public partial class ShutdownCheckView : UserControl
{
public ShutdownCheckView()
{
this.InitializeComponent();
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- This manifest is used on Windows only. <!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls. Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.1.0.0" name="MayShow.Desktop"/> <assemblyIdentity version="1.2.0.0" name="MayShow.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>