67 Commits

Author SHA1 Message Date
mbabienco 60361b4913 Only use AoT on not debug (fixes Avalonia previewer) 2026-04-28 17:21:27 +09:00
mbabienco 42f7de5d5c Start work on copy files to internal 2026-04-26 22:56:10 +09:00
mbabienco 236eccf3d1 Better error handling on choose file loc 2026-04-24 12:02:04 +09:00
mbabienco e2408ce581 Set suggested file name for save window 2026-04-24 12:01:01 +09:00
mbabienco b0c897bef9 Rename output settings to be more descriptive 2026-04-24 11:59:57 +09:00
mbabienco 82e3289e21 Fix using wrong output file path 2026-04-24 11:59:45 +09:00
mbabienco 004647008f Refactor PDF file name logic and error checks 2026-04-24 11:50:39 +09:00
mbabienco 981114a222 Start working on letting user pick output location
Not done yet. May need to refactor some code before CreatePDF() is called and do the file handling stuff before
2026-04-23 11:48:30 +09:00
mbabienco e813504453 Add new enum for where PDF output file is saved 2026-04-23 11:24:04 +09:00
mbabienco 71ca0c6459 Turn on accent for save button when unsaved work 2026-04-23 11:00:51 +09:00
mbabienco d5b174fa22 Update TODO.txt 2026-04-22 21:22:24 +09:00
mbabienco a58560efb3 Update TODO.txt 2026-04-22 17:56:30 +09:00
mbabienco cd9fce14fc Remove unneeded NotifyPropertyChanged 2026-04-22 17:23:56 +09:00
mbabienco 135647c591 Fix home screen UI grid use 2026-04-22 17:22:30 +09:00
mbabienco ed26989678 Refactor mime types, apple type identifiers, file picker type filter 2026-04-22 17:20:19 +09:00
mbabienco 287ef24113 Base folder always set to internal dir; create rpt view assumes working folder is rpt BaseFolder 2026-04-22 17:16:45 +09:00
mbabienco 8a502ce820 Move more recent saved data to internal 2026-04-22 11:44:27 +09:00
mbabienco d5c188ed2e Add Utilities. SaveReportDataSync 2026-04-22 11:44:10 +09:00
mbabienco eae459e047 Remove some unneeded using statements 2026-04-22 11:44:02 +09:00
mbabienco c5e3276f34 Get rid of SaveReportJsonDataInInternalDir option 2026-04-22 11:01:37 +09:00
mbabienco afed4c8438 Always save data to internal directory 2026-04-22 10:58:16 +09:00
mbabienco c3ec91027f Make sure UUIDs in sync when upgrade settings 2026-04-22 10:48:06 +09:00
mbabienco 411d70aa18 UI tweaks 2026-04-21 11:41:44 +09:00
mbabienco 14fc5b29ee Add back button to create report view; fix change title not warning about unsaved 2026-04-21 11:31:33 +09:00
mbabienco d99fefe74d Move settings and about to main screen 2026-04-21 11:23:28 +09:00
mbabienco dd09459226 Disable unused field warning 2026-04-21 11:18:40 +09:00
mbabienco c3d6c0c846 Clean up some using statements 2026-04-21 11:13:05 +09:00
mbabienco 69a885ff57 Fix TopLevelGrabber not avail in create report vm 2026-04-21 11:01:46 +09:00
mbabienco 81a11a9b84 Don't use certain text trimming (bug: Avalonia #20535) 2026-04-21 10:51:26 +09:00
mbabienco 4dfcd991a6 Duplicate icon file to another spot 2026-04-21 10:50:53 +09:00
mbabienco f2c4d1ed53 Fix wrong using 2026-04-21 10:50:47 +09:00
mbabienco e47d4bcbc1 Bump Magick.NET 2026-04-21 10:50:35 +09:00
mbabienco 7be1e16489 Finish merge from main with MainViewModel changes 2026-04-21 10:41:40 +09:00
mbabienco 73e959a677 Merge branch 'main' into feature/ios 2026-04-21 10:41:13 +09:00
mbabienco fe6b8c684f Create organizing-thoughts.txt 2026-04-08 12:28:58 +09:00
mbabienco ddba84ac0b Remove some TODO 2026-04-04 21:46:37 +09:00
mbabienco b0661669b4 Update main menu recently used on save 2026-04-04 21:45:12 +09:00
mbabienco 249cc8724d Update TODO.txt 2026-04-04 21:10:55 +09:00
mbabienco 3fddda26c9 Continue refactor to use PDFReport obj in CreatePDFReportViewModel 2026-04-04 21:10:49 +09:00
mbabienco e7c8328630 Update TODO.txt 2026-04-03 18:17:35 +09:00
mbabienco 9391e868a2 Continue tweaking code to work with main menu
Main menu now allows start blank, start with existing, and load previous.
Going to rework the CreatePDFReportViewModel to basically take that report obj and run with it (scan the folder, etc.).
2026-04-03 18:17:29 +09:00
mbabienco a0ef69e8ea PDFReport files is an observable collection 2026-04-03 18:16:43 +09:00
mbabienco 2092f15580 Add method to get unique report GUID 2026-04-03 18:16:28 +09:00
mbabienco a2452e2585 Small code tweaks for readability 2026-04-03 10:06:14 +09:00
mbabienco 3e0852ead9 Remove unused usings 2026-04-03 10:01:57 +09:00
mbabienco faf9fc488e Main new/choose report VM impls ICanCheckShutdown 2026-04-03 09:59:39 +09:00
mbabienco b22dbdff03 Refactor PDF gen to its own file
Keeps PDF logic out of main view model for easier rework of report data saving/loading
2026-04-03 09:59:22 +09:00
mbabienco 1785a91d6d Sort receipts by date then title 2026-04-01 18:45:58 +09:00
mbabienco 22d2a0d5e0 Refactor a const 2026-03-30 21:16:17 +09:00
mbabienco 4c85b4b606 Refactor make paragraph code and conv file path 2026-03-30 21:14:26 +09:00
mbabienco 22d896543c Keep doing rework for using PDFReportInfo obj
Still need to think about how this all works...could probably simplify more...
2026-03-30 21:00:19 +09:00
mbabienco 534af58f44 Refactor font resolving to its own class/file 2026-03-30 20:24:36 +09:00
mbabienco a9674a3f45 Continue working on main menu screen and refactor
New method for storing information on report data; rework and migrate settings to version 2; Add new options to ConfirmViewModel; rework some class hierarchy (may change more later)
2026-03-30 20:12:34 +09:00
mbabienco 8be518e81c Call base() constructor for a bunch of objs 2026-03-30 19:40:15 +09:00
mbabienco a072bff548 Bump Magick.NET-Q8-AnyCPU version 2026-03-27 15:02:15 +09:00
mbabienco fa82407915 Start on new first view for mobile
Allows user to start new report or load an existing report. Currently showing some dummy data; UI is not finished at this point. Still need to get settings for this saving/loading/etc.

May or may not provide option to use this on desktop...would certainly make the software more self-contained...
2026-03-24 21:44:45 +09:00
mbabienco fcee3b09e6 Remove unneeded Console.WriteLine 2026-03-24 20:48:35 +09:00
mbabienco b8b1effcf2 Make sure version up to date 2026-03-24 20:47:25 +09:00
mbabienco e71de9016e Merge branch 'main' into feature/ios 2026-03-24 20:46:51 +09:00
mbabienco c53b41658a Update TODO.txt 2026-03-05 12:16:25 +09:00
mbabienco 80eda8bc5b Add some comments 2026-03-05 09:34:36 +09:00
mbabienco 7d4c440176 Clean up initial view code 2026-03-05 09:24:35 +09:00
mbabienco b0cc2c4f6b Rework view hierarchy for iOS 2026-03-04 22:10:29 +09:00
mbabienco 9e7ce7ef57 Fix wrong About menu title 2026-03-04 21:10:15 +09:00
mbabienco 385f794b03 Rearrange files to shared project 2026-03-04 21:09:54 +09:00
mbabienco 182055d19e Add iOS project 2026-03-04 17:56:23 +09:00
mbabienco c258584504 Move Desktop to subfolder and add sln 2026-03-04 17:55:33 +09:00
92 changed files with 2619 additions and 1675 deletions
+55
View File
@@ -1,3 +1,58 @@
----iOS----
-quickstart for loading last edited report on main menu
-duplicate existing report with new name
-cleanup empty uuid folders in case user gets an internal folder created but never saves
*-always save report data (file locations, title, etc.) to internal dir (might already be done?)
-no more option to save data internally -> ALWAYS save report_data.json internally
*-update project title -> should update recently used data
-this sort of works, something is wrong with the upgrade process where the UUID is not brought over properly; need to test and fix (maybe fixed already and my dataset is wrong?)
*-add dropdown to Add Items button to have add items from folder and remove choose working folder option (data always saved internally)
-not going to do this; they can always select multiple items from the current picker
*-Now that the BaseFolder is always internal...update options for PDF output location
-output in internal dir (default)
-always ask me every time (opens save file picker after generating PDF to ask user where they want it)
-always put in X folder (uses existing option)
~-add option to backup added files to internal data directory (always on for iOS)
*-add option
-add to settings view
-add logic to CreatePDFReportViewModel
-programmed in but not tested yet
-make backup of last generated PDF somewhere (another internal folder, most likely);
-save to PDF report data somewhere for safe-keeping
-allow loading by user
-iOS-specific (MAUI essentials?)
-maui community essentials https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/get-started?tabs=CommunityToolkitMaui
-I don't think this is what we are looking for ultimately....?
-https://github.com/AvaloniaUI/Avalonia.Essentials
-https://github.com/AvaloniaUI/AvaloniaMauiHybrid -- this may have to be the way to go or perhaps grab code from https://github.com/AvaloniaUI/Avalonia.Controls.Maui/tree/main/src/Avalonia.Controls.Maui.Essentials because we don't need the avalonia tie in stuff if it "just works"...but don't know if it will "just work" yet. apparently native APIs are available through net10.0-ios or whatnot but I'd rather use the existing wrappers/tutorials since those will just work just like we want...
-auto save on every action
-Take picture
-https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/device-media/picker?view=net-maui-10.0&tabs=macios
-Add pic from gallery
-see above link
-Add file (uses Files picking, I think this already mostly works just need to copy to internal data dir)
-Generate PDF -> Share/print screen to share/print
-https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/data/share?view=net-maui-10.0&tabs=macios
-Make sure clipboard works (not sure if it will out of the box or not)
-https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/data/clipboard?view=net-maui-10.0
-show report file size in recent list
-make sure setting for backup files is not available (always backs up files on iOS)
-----------
https://stackoverflow.com/questions/78855900/error-ios-projects-must-build-with-publishtrimmed-true-when-trimming-is-disabl
https://blog.verslu.is/maui/exclude-assemblies-from-trimming/
-p:UseParallelGC=true for faster builds? or /p:MtouchUseLlvm=false? or -p:EnableAssemblyILStripping=false?
possible hints for building publish iOS binaries: https://github.com/dotnet/maui/issues/25022#issuecomment-2385334527
linker stuff: https://blog.verslu.is/maui/exclude-assemblies-from-trimming/
PublishFolderType documentation: https://github.com/dotnet/macios/blob/main/dotnet/BundleContents.md
https://signpath.org/
magick "2026-01-29 — \$210 — WORK PERMIT.pdf" -background white -alpha background -alpha off -density 288 -resize 25% page-%03d.jpg magick "2026-01-29 — \$210 — WORK PERMIT.pdf" -background white -alpha background -alpha off -density 288 -resize 25% page-%03d.jpg
https://github.com/dlemstra/Magick.NET/blob/main/docs/ConvertPDF.md https://github.com/dlemstra/Magick.NET/blob/main/docs/ConvertPDF.md
https://stackoverflow.com/questions/65089839/add-an-external-pdf-page-to-pdfsharp-migradoc https://stackoverflow.com/questions/65089839/add-an-external-pdf-page-to-pdfsharp-migradoc
+13
View File
@@ -0,0 +1,13 @@
Three ways to get started with a report:
1. Pick a folder on disk
-> No title, empty list, have base folder to work in
-> Generate UUID, make sure it's unique among all reports that we have
-> on first save or add file (if copying to local), only create internal folder if needed (we know UUID is unique so we are OK if the setting changes later)
2. Pick an already-saved report (that is either in local dir or somewhere else on disk)
-> Have title (presumably), have file list already, have UUID already with internal folder if saved locally, etc. everything basically ready to go
3. Start new, empty report with title
-> Have title, empty list
-> Generate UUID, make sure it's unique among all reports that we have
-> on first save or add file (if copying to local), create folder -> save data
-65
View File
@@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Tmds.DBus.Protocol;
namespace MayShow.Helpers;
class Utilities
{
public static JsonSerializerOptions GetSerializerOptions()
{
var opts = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
return opts;
}
public static DateOnly? CheckValidDateInString(string str)
{
// https://stackoverflow.com/a/14918404/3938401
// formats = regex format -> DateTime parsing format
var formats = new Dictionary<string, string>
{
{@"\d{4}-\d{2}-\d{2}", "yyyy-MM-dd"},
{@"\d{4}.d{2}.d{2}", "yyyy.MM.dd"},
{@"\d{8}", "yyyyMMdd"}
};
foreach (var data in formats)
{
var rgx = new Regex(data.Key);
var mat = rgx.Match(str);
if (mat.Success)
{
var dtStr = mat.ToString();
var didWork = DateTime.TryParseExact(dtStr, [data.Value], CultureInfo.InvariantCulture,
DateTimeStyles.None, out var parsedDateTime);
if (didWork)
{
return DateOnly.FromDateTime(parsedDateTime);
}
}
}
return null;
}
public static string GetInternalDataPath()
{
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MayShow"
);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path;
}
}

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 401 KiB

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
<PublishAot Condition="'$(Configuration)' != 'Debug'">true</PublishAot>
<AssemblyName>MayShow.Desktop</AssemblyName>
<AssemblyVersion>1.4.2</AssemblyVersion> <!-- Also update Constants version -->
<ApplicationIcon>MayShow-icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<ProjectReference Include="..\MayShow.Shared\MayShow.Shared.csproj" />
</ItemGroup>
</Project>
@@ -3,7 +3,7 @@ using System;
namespace MayShow; namespace MayShow;
class Program sealed class Program
{ {
// Initialization code. Don't use any Avalonia, third-party APIs or any // Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
@@ -85,8 +85,8 @@
</Style> </Style>
</Application.Styles> </Application.Styles>
<Application.DataTemplates> <Application.DataTemplates>
<DataTemplate DataType="{x:Type viewModels:MainViewModel}"> <DataTemplate DataType="{x:Type viewModels:CreatePDFReportViewModel}">
<views:MainView/> <views:CreatePDFReportView />
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WarningDeleteItemViewModel}"> <DataTemplate DataType="{x:Type viewModels:WarningDeleteItemViewModel}">
<views:WarningDeleteItem/> <views:WarningDeleteItem/>
@@ -109,6 +109,9 @@
<DataTemplate DataType="{x:Type viewModels:SettingsViewModel}"> <DataTemplate DataType="{x:Type viewModels:SettingsViewModel}">
<views:SettingsView/> <views:SettingsView/>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type viewModels:StartNewChooseReportViewModel}">
<views:StartNewChooseReport/>
</DataTemplate>
</Application.DataTemplates> </Application.DataTemplates>
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
@@ -1,31 +1,47 @@
using System; using System;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using DialogHostAvalonia; using DialogHostAvalonia;
using MayShow; using MayShow;
using MayShow.Interfaces;
using MayShow.Views;
using MayShow.ViewModels; using MayShow.ViewModels;
namespace MayShow; namespace MayShow;
public partial class App : Application public partial class App : Application, ITopLevelGrabber
{ {
public override void Initialize() public override void Initialize()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private TopLevel? _topLevel;
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow(); desktop.MainWindow = new MainWindow();
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView();
_topLevel = TopLevel.GetTopLevel(singleViewPlatform.MainView);
singleViewPlatform.MainView.DataContext = new MainViewModel(this);
}
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
public TopLevel GetTopLevel()
{
return _topLevel!;
}
public void AboutOnClick(object? sender, EventArgs args) public void AboutOnClick(object? sender, EventArgs args)
{ {
DialogHost.Show(new AboutViewModel()); DialogHost.Show(new AboutViewModel());
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

@@ -0,0 +1,8 @@
namespace MayShow.Enums;
enum PDFSaveLocation : ushort
{
BaseFolder = 0,
AlwaysAsk = 1,
OtherChosenDir = 2,
}
@@ -11,6 +11,9 @@ class Constants
public static string[] AllowedFileExtensionPatterns = [ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp", "*.pdf", "*.heic", ]; 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[] AllowedFileExtensionsNoStar = [ "png", "jpg", "jpeg", "gif", "bmp", "webp", "pdf", "heic", ];
public static string[] FilePickerMimeTypes = [ "image/*", "application/pdf", "image/heic" ];
public static string[] FilePickerAppleTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ];
public static string ReportSavedDataFileName = "report_data.json"; public static string ReportSavedDataFileName = "report_data.json";
public static List<DateDisplayFormat> GetDateDisplayFormats() public static List<DateDisplayFormat> GetDateDisplayFormats()
@@ -15,7 +15,7 @@ class DataGridDropHandler : BaseDataGridDropHandler<ReportFile>
protected override bool Validate(DataGrid dg, DragEventArgs e, object? sourceContext, object? targetContext, bool execute) protected override bool Validate(DataGrid dg, DragEventArgs e, object? sourceContext, object? targetContext, bool execute)
{ {
if (sourceContext is not ReportFile sourceItem if (sourceContext is not ReportFile sourceItem
|| targetContext is not MainViewModel vm || targetContext is not CreatePDFReportViewModel vm
|| dg.GetVisualAt(e.GetPosition(dg)) is not Control targetControl || dg.GetVisualAt(e.GetPosition(dg)) is not Control targetControl
|| targetControl.DataContext is not ReportFile targetItem) || targetControl.DataContext is not ReportFile targetItem)
{ {
@@ -0,0 +1,82 @@
using System.IO;
using MayShow.Interfaces;
using PdfSharp.Fonts;
namespace MayShow.Helpers;
class PDFFontResolver : IFontResolver
{
private string _runningProcessDirectory;
private ILogger? _logger;
public PDFFontResolver(string runningProcessDirectory, ILogger? logger)
{
_logger = logger;
_runningProcessDirectory = runningProcessDirectory;
}
public byte[]? GetFont(string faceName)
{
_logger?.LogInfo(string.Format("Loading font {0}", faceName));
if (faceName == "Noto Sans")
{
var path = Path.Combine(_runningProcessDirectory, "Assets/Fonts/Noto_Sans/static/NotoSans-Regular.ttf");
if (!File.Exists(path))
{
path = Path.Combine(_runningProcessDirectory, "../Resources/Assets/Fonts/Noto_Sans/static/NotoSans-Regular.ttf");
}
return File.ReadAllBytes(path);
}
if (faceName == "Noto Sans Bold")
{
var path = Path.Combine(_runningProcessDirectory, "Assets/Fonts/Noto_Sans/static/NotoSans-Bold.ttf");
if (!File.Exists(path))
{
path = Path.Combine(_runningProcessDirectory, "../Resources/Assets/Fonts/Noto_Sans/static/NotoSans-Bold.ttf");
}
return File.ReadAllBytes(path);
}
if (faceName == "Noto Sans JP")
{
var path = Path.Combine(_runningProcessDirectory, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
if (!File.Exists(path))
{
path = Path.Combine(_runningProcessDirectory, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
}
return File.ReadAllBytes(path);
}
if (faceName == "Noto Sans JP Bold")
{
var path = Path.Combine(_runningProcessDirectory, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
if (!File.Exists(path))
{
path = Path.Combine(_runningProcessDirectory, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
}
return File.ReadAllBytes(path);
}
return null;
}
public FontResolverInfo? ResolveTypeface(string familyName, bool bold, bool italic)
{
// LogInfo(string.Format("Resolving font name {0}", familyName));
if (familyName == "Noto Sans")
{
if (bold)
{
return new FontResolverInfo(familyName + " Bold");
}
return new FontResolverInfo(familyName);
}
if (familyName == "Noto Sans JP")
{
if (bold)
{
return new FontResolverInfo(familyName + " Bold");
}
return new FontResolverInfo(familyName);
}
return null;
}
}
+164
View File
@@ -0,0 +1,164 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using MayShow.Models;
namespace MayShow.Helpers;
class Utilities
{
public static JsonSerializerOptions GetSerializerOptions()
{
var opts = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
return opts;
}
public static DateOnly? CheckValidDateInString(string str)
{
// https://stackoverflow.com/a/14918404/3938401
// formats = regex format -> DateTime parsing format
var formats = new Dictionary<string, string>
{
{@"\d{4}-\d{2}-\d{2}", "yyyy-MM-dd"},
{@"\d{4}.d{2}.d{2}", "yyyy.MM.dd"},
{@"\d{8}", "yyyyMMdd"}
};
foreach (var data in formats)
{
var rgx = new Regex(data.Key);
var mat = rgx.Match(str);
if (mat.Success)
{
var dtStr = mat.ToString();
var didWork = DateTime.TryParseExact(dtStr, [data.Value], CultureInfo.InvariantCulture,
DateTimeStyles.None, out var parsedDateTime);
if (didWork)
{
return DateOnly.FromDateTime(parsedDateTime);
}
}
}
return null;
}
public static string GetInternalDataPath()
{
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MayShow"
);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path;
}
public static string GetTempConvertedImagesFolderPath()
{
// get converted files directory path and create it if necessary
var convertedDir = Path.Combine(GetInternalDataPath(), "converted");
if (!Directory.Exists(convertedDir))
{
Directory.CreateDirectory(convertedDir);
}
return convertedDir;
}
public static Guid GetUniqueReportGuid(Settings settings)
{
// Guid should be, well, unique already, BUT we are not going to take ANY chances.
var internalPath = GetInternalDataPath();
Guid guid = Guid.NewGuid();
var isUnique = false;
while (!isUnique)
{
var strUUID = guid.ToString();
var didFind = false;
foreach (var existingReport in settings.AllReportInfo)
{
if (existingReport.UUID == strUUID)
{
didFind = true;
break;
}
}
if (Directory.Exists(Path.Combine(internalPath, strUUID)))
{
didFind = true;
}
if (!didFind)
{
isUnique = true;
}
else
{
guid = Guid.NewGuid();
}
}
return guid;
}
public static void SaveReportDataSync(PDFReport reportData, string path, JsonTypeInfo<PDFReport>? context)
{
if (context == null)
{
var jsonContext = new SourceGenerationContext(GetSerializerOptions());
context = jsonContext.PDFReport;
}
using var memoryStream = new MemoryStream();
JsonSerializer.Serialize(memoryStream, reportData, context);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var updatedJson = reader.ReadToEnd();
File.WriteAllText(path, updatedJson);
}
public static async Task SaveReportDataAsync(PDFReport reportData, string path, JsonTypeInfo<PDFReport>? context = null)
{
if (context == null)
{
var jsonContext = new SourceGenerationContext(GetSerializerOptions());
context = jsonContext.PDFReport;
}
using var memoryStream = new MemoryStream();
await JsonSerializer.SerializeAsync(memoryStream, reportData, context);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var json = await reader.ReadToEndAsync();
await File.WriteAllTextAsync(path, json);
}
public static FilePickerFileType[] GetReportFilePickerFileTypes()
{
return [
new FilePickerFileType("All Types")
{
Patterns = Constants.AllowedFileExtensionPatterns,
AppleUniformTypeIdentifiers = Constants.FilePickerAppleTypeIdentifiers,
MimeTypes = Constants.FilePickerMimeTypes,
},
FilePickerFileTypes.ImageAll,
new FilePickerFileType("HEIC Images")
{
Patterns = [ "*.heic" ],
AppleUniformTypeIdentifiers = [ "public.heic" ],
MimeTypes = [ "image/heic" ]
},
FilePickerFileTypes.Pdf,
];
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace MayShow.Interfaces;
interface ILogger
{
void LogInfo(string message, params object[]? arguments);
}
@@ -0,0 +1,8 @@
using MayShow.Models;
namespace MayShow.Interfaces;
interface IUpdateRecentlyUsed
{
void UpdateRecentlyUsed(PDFReport report);
}
@@ -5,20 +5,13 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MayShow.MainWindow" x:Class="MayShow.MainWindow"
Title="MayShow" Title="MayShow"
xmlns:views="clr-namespace:MayShow.Views"
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:MainWindowViewModel" x:DataType="vm:MainViewModel"
Width="800" Width="800"
MinWidth="550" MinWidth="550"
Height="650" Height="650"
MinHeight="550"> MinHeight="550">
<dialogHost:DialogHost CloseOnClickAway="False" <views:MainView/>
Identifier="DialogHost"
x:Name="WindowDialogHost">
<dialogHost:DialogHost.DialogContent>
<StackPanel/>
</dialogHost:DialogHost.DialogContent>
<!-- put the content over which the dialog is shown here (e.g. your main window grid)-->
<ContentControl Content="{Binding CurrentViewModel}"/>
</dialogHost:DialogHost>
</Window> </Window>
@@ -14,7 +14,7 @@ public partial class MainWindow : Window, ITopLevelGrabber
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
DataContext = new MainWindowViewModel(this); DataContext = new MainViewModel(this);
Closing += WindowIsClosing; Closing += WindowIsClosing;
@@ -49,14 +49,14 @@ public partial class MainWindow : Window, ITopLevelGrabber
private async Task<bool> CheckIfClosePossible() private async Task<bool> CheckIfClosePossible()
{ {
var canShutdown = true; var canShutdown = true;
if (DataContext is MainWindowViewModel mwvm) if (DataContext is MainViewModel mvm)
{ {
if (mwvm is ICanCheckShutdown canCheck) if (mvm is ICanCheckShutdown canCheck)
{ {
canShutdown = await canCheck.CheckIsSafeToShutdown(); canShutdown = await canCheck.CheckIsSafeToShutdown();
} }
// only checking 1 level but for this app that is OK // only checking 1 level but for this app that is OK
if (canShutdown && mwvm.CurrentViewModel is ICanCheckShutdown currModel) if (canShutdown && mvm.CurrentViewModel is ICanCheckShutdown currModel)
{ {
try try
{ {
Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

@@ -1,16 +1,10 @@

<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot>
<AssemblyName>MayShow</AssemblyName> <AssemblyName>MayShow</AssemblyName>
<AssemblyVersion>1.4.3</AssemblyVersion> <!-- Also update Constants version --> <AssemblyVersion>1.4.3</AssemblyVersion> <!-- Also update Constants version -->
<ApplicationIcon>MayShow-icon.ico</ApplicationIcon> <ApplicationIcon>MayShow-icon.ico</ApplicationIcon>
@@ -30,21 +24,27 @@
<ItemGroup> <ItemGroup>
<Content Include="Assets\Fonts\Noto_Sans\*.*"> <Content Include="Assets\Fonts\Noto_Sans\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
<Content Include="Assets\Fonts\Noto_Sans\static\*.*"> <Content Include="Assets\Fonts\Noto_Sans\static\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
<Content Include="Assets\Fonts\Noto_Sans_JP\*.*"> <Content Include="Assets\Fonts\Noto_Sans_JP\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
<Content Include="Assets\Fonts\Noto_Sans_JP\static\*.*"> <Content Include="Assets\Fonts\Noto_Sans_JP\static\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
<Content Include="Assets\Fonts\FontAwesome\*.*"> <Content Include="Assets\Fonts\FontAwesome\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
<Content Include="Assets\LICENSES.txt"> <Content Include="Assets\LICENSES.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PublishFolderType>Resource</PublishFolderType>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -57,8 +57,9 @@
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" /> <PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.11.1" Condition="'$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.12.0" Condition="'$(RuntimeIdentifier)' != 'osx-x64'" />
<!-- DO NOT UPDATE BEYOND 14.9.1 OR YOU WILL BREAK macOS MONTEREY USERS --> <!-- DO NOT UPDATE BEYOND 14.9.1 OR YOU WILL BREAK macOS MONTEREY USERS -->
<PackageReference Include="Magick.NET-Q8-x64" Version="14.9.1" Condition="'$(RuntimeIdentifier)' == 'osx-x64'" /> <PackageReference Include="Magick.NET-Q8-x64" Version="14.9.1" Condition="'$(RuntimeIdentifier)' == 'osx-x64'" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" Version="0.10.11-preview20251127001" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing" Version="0.10.11-preview20251127001" />
+38
View File
@@ -0,0 +1,38 @@
using System;
using System.Collections.ObjectModel;
namespace MayShow.Models;
class PDFReport : PDFReportInfo
{
private ObservableCollection<ReportFile> _files;
private DateTime? _lastGenerated;
public PDFReport() : base()
{
_files = [];
_lastGenerated = null;
}
public PDFReport(PDFReportInfo info) : base()
{
_files = [];
_lastGenerated = null;
BaseFolder = info.BaseFolder;
UUID = info.UUID;
Title = info.Title;
LastSaved = info.LastSaved;
}
public ObservableCollection<ReportFile> Files
{
get => _files;
set { _files = value; NotifyPropertyChanged(); }
}
public DateTime? LastGenerated
{
get => _lastGenerated;
set { _lastGenerated = value; NotifyPropertyChanged(); }
}
}
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using MayShow.Helpers;
namespace MayShow.Models;
class PDFReportInfo : ChangeNotifier
{
private string _baseFolder;
private string _uuid;
private string _title;
private DateTime? _lastSaved;
public PDFReportInfo() : base()
{
_uuid = Guid.NewGuid().ToString();
_baseFolder = "";
UpdateBaseFolder();
_title = "";
_lastSaved = null;
}
public string BaseFolder
{
get => _baseFolder;
set { _baseFolder = value; NotifyPropertyChanged(); }
}
public string UUID
{
get => _uuid;
set { _uuid = value; NotifyPropertyChanged(); }
}
public string Title
{
get => _title;
set { _title = value; NotifyPropertyChanged(); }
}
public DateTime? LastSaved
{
get => _lastSaved;
set { _lastSaved = value; NotifyPropertyChanged(); }
}
public void UpdateBaseFolder()
{
_baseFolder = Path.Combine(Utilities.GetInternalDataPath(), _uuid);
}
public void ResetUUID()
{
UUID = Guid.NewGuid().ToString();
}
public string GetWorkingPath()
{
if (string.IsNullOrWhiteSpace(BaseFolder))
{
return Path.Combine(Utilities.GetInternalDataPath(), UUID);
}
return BaseFolder;
}
public void DeleteInternalFolderFromDisk()
{
var path = Path.Combine(Utilities.GetInternalDataPath(), UUID);
if (Directory.Exists(path) && path != Utilities.GetInternalDataPath())
{
Directory.Delete(path, true);
}
}
}
@@ -12,7 +12,7 @@ class ReportFile : ChangeNotifier
private string _notes; private string _notes;
private string _filePath; private string _filePath;
public ReportFile() public ReportFile() : base()
{ {
_title = ""; _title = "";
_receiptDateTime = DateTime.Now; _receiptDateTime = DateTime.Now;
@@ -0,0 +1,391 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using DialogHostAvalonia;
using Docnet.Core;
using Docnet.Core.Models;
using Docnet.Core.Readers;
using ImageMagick;
using MayShow.Helpers;
using MayShow.Interfaces;
using MayShow.ViewModels;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using PdfSharp.Fonts;
using PdfSharp.Pdf.IO;
using PdfSharp.Snippets.Font;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace MayShow.Models;
class ReportPDFCreator : ChangeNotifier
{
private ILogger _logger;
public ReportPDFCreator(ILogger logger)
{
_logger = logger;
}
private Paragraph GetFooterParagraph()
{
var footerPar = new Paragraph();
footerPar.Format.Alignment = ParagraphAlignment.Center;
footerPar.Format.Font.Size = 10;
footerPar.AddText("--Page ");
footerPar.AddPageField();
footerPar.AddText(" of ");
footerPar.AddNumPagesField();
footerPar.AddText("--");
footerPar.AddLineBreak();
footerPar.AddText("Report generated on " + DateTime.Now.ToString("f"));
footerPar.Tag = "FooterPar";
footerPar.Format.Font.Name = "Noto Sans";
return footerPar;
}
private decimal GetExistingPageItemHeight(PdfDocumentRenderer pdfRenderer, decimal footerParagraphHeight)
{
pdfRenderer.DocumentRenderer.PrepareDocument();
var currPageCount = pdfRenderer.DocumentRenderer.FormattedDocument?.PageCount;
var heightForExistingItemsOnPage = footerParagraphHeight;
if (currPageCount.HasValue)
{
var renderInfo = pdfRenderer.DocumentRenderer.GetRenderInfoFromPage(currPageCount.Value);
if (renderInfo != null)
{
// Console.WriteLine("Got render info for page: {0}", currPageCount);
foreach (var item in renderInfo)
{
heightForExistingItemsOnPage += (decimal)item.LayoutInfo.ContentArea.Height.Inch;
}
}
}
return heightForExistingItemsOnPage;
}
private Paragraph MakeParagraph(Section section, string text, bool isBold, int fontSize, string tag, bool isCenter = true)
{
const string defaultFontName = "Noto Sans JP";
var par = section.AddParagraph();
par.Format.Alignment = isCenter ? ParagraphAlignment.Center : ParagraphAlignment.Left;
par.Format.Font.Size = fontSize;
par.Format.Font.Bold = isBold;
par.Format.Font.Name = defaultFontName; // has english letters in it, too
par.AddText(text);
par.Tag = tag;
return par;
}
// https://forum.pdfsharp.net/viewtopic.php?f=2&t=1025
public async Task<string?> CreatePDF(List<ReportFile> reportFiles, string reportTitle, string outputFilePathWithName, PDFFontResolver fontResolver, Settings appSettings)
{
// setup globals and consts...
GlobalFontSettings.FontResolver = fontResolver;
GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
const int maxImageWidth = 425;
const decimal pageWidth = 8.5m;
const decimal pageHeight = 11.0m;
const decimal margin = 0.5m;
const int imageResolution = 72;
const int imageInsertMarginPixels = 30; // we calculate max available; use max - this # for max image size
const decimal reduceImageSizeAmount = 0.95m;
var maxItemPxWidth = ((pageWidth - (2 * margin)) * imageResolution) - imageInsertMarginPixels;
var imageLineFormat = new MigraDoc.DocumentObjectModel.Shapes.LineFormat()
{
Color = Colors.Black,
Width = Unit.FromPoint(2),
};;
// start making PDF!
var convertedDir = Utilities.GetTempConvertedImagesFolderPath();
// create doc and setup initial section (for page characteristics)
var pdfDoc = new Document();
var section = pdfDoc.AddSection();
section.PageSetup.PageFormat = PageFormat.Letter;
section.PageSetup.PageWidth = pageWidth + "in";
section.PageSetup.PageHeight = pageHeight + "in";
section.PageSetup.TopMargin = margin + "in";
section.PageSetup.RightMargin = margin + "in";
section.PageSetup.BottomMargin = margin + "in";
section.PageSetup.LeftMargin = margin + "in";
// setup footer for page number
var footerPar = GetFooterParagraph();
section.Footers.Primary.Add(footerPar);
// create a quick PDF doc renderer to measure footer paragraph height
var footerParagraphHeight = 0.4m; // estimate
var footerOnlyPdfDoc = new Document();
var sectionClone = section.Clone();
footerOnlyPdfDoc.Add(sectionClone);
sectionClone.Add(GetFooterParagraph());
var footerPdfRenderer = new PdfDocumentRenderer
{
Document = footerOnlyPdfDoc
};
footerPdfRenderer.DocumentRenderer.PrepareDocument();
var footerRenderInfo = footerPdfRenderer.DocumentRenderer.GetRenderInfoFromPage(1);
if (footerRenderInfo != null)
{
foreach (var item in footerRenderInfo)
{
if (item.DocumentObject.Tag?.ToString() == "FooterPar")
{
Console.WriteLine("Got footer paragraph height!");
footerParagraphHeight = (decimal)item.LayoutInfo.ContentArea.Height.Inch;
break;
}
}
}
// continue setting up document
// First page only: add report title
MakeParagraph(section, reportTitle, true, 16, "TitlePar");
//
var outputFilePathNoName = Path.GetDirectoryName(outputFilePathWithName) ?? Utilities.GetInternalDataPath();
var outputFileName = Path.GetFileName(outputFilePathWithName);
var pdfRenderer = new PdfDocumentRenderer
{
Document = pdfDoc,
WorkingDirectory = outputFilePathNoName
};
var hasAddedData = false;
for (var i = 0; i < reportFiles.Count; i++)
{
var file = reportFiles[i];
var fileName = file.FileName;
var filePath = file.FilePath;
if (!File.Exists(filePath))
{
_logger?.LogInfo("ERROR: File \"{0}\" does not exist at path \"{1}\". Please remove it from the report or re-add it using the Add Item button if you still want it to be in this report.", file.Title, file.FilePath);
return null;
}
if (fileName == ".DS_Store" || fileName == outputFileName)
{
continue;
}
if (i > 0 && hasAddedData)
{
section.AddPageBreak();
}
var imageTitle = string.IsNullOrWhiteSpace(file.Title) ? file.FileName : file.Title;
var imageTitlePar = MakeParagraph(section, imageTitle, true, 12, "ReceiptTitlePar");
MakeParagraph(section, file.ReceiptDate.ToString(appSettings.ReportDateFormat), true, 12, "ReceiptDatePar");
if (!string.IsNullOrWhiteSpace(file.Notes))
{
var imageNotesPar = MakeParagraph(section, file.Notes, false, 10, "ReceiptNotesPar");
}
var emptyPar = section.AddParagraph(); // add empty line for spacing
emptyPar.Tag = "EmptyParagraph";
// now add the image
var lowerName = fileName.ToLower();
var isPDF = lowerName.EndsWith(".pdf");
// convert heic, webp, or png to JPEG for size and ease of use
// (and probably compat reasons too, though I haven't tested that...)
var isHEIC = lowerName.EndsWith(".heic");
var isWebp = lowerName.EndsWith(".webp");
var isPNG = lowerName.EndsWith(".png");
var info = new FileInfo(file.FilePath);
uint loadedImageWidth = 0;
uint loadedImageHeight = 0;
// get max pixel height remaining for items on this page
// (For multi-page PDFs, showing page 2 and on will have more height since they have no title,
// but to keep things consistent we will use the same height for all PDF pages.)
// render up to now on this page and get height remaining in inches
var currPageCount = pdfRenderer.DocumentRenderer.FormattedDocument?.PageCount;
var heightForExistingItemsOnPage = GetExistingPageItemHeight(pdfRenderer, footerParagraphHeight);
var remainingHeightInches = pageHeight - (2 * margin) - heightForExistingItemsOnPage;
var remainingHeightPixels = (remainingHeightInches * imageResolution) - imageInsertMarginPixels;
if (!isPDF)
{
using var mImage = new MagickImage(info.FullName);
loadedImageWidth = mImage.Width;
loadedImageHeight = mImage.Height;
var convertedOutputPath = Path.Combine(convertedDir, info.Name + ".jpg");
var didAdjust = false;
_logger?.LogInfo("Image orientation of {0} is {1}", fileName, mImage.Orientation);
if (mImage.Orientation != OrientationType.TopLeft)
{
_logger?.LogInfo("Auto-adjusted image orientation of {0}", fileName);
mImage.AutoOrient();
didAdjust = true;
}
// perform needed image manipulations
if (isHEIC || isWebp || isPNG || (!isPDF && info.Length > appSettings.ImageResizeThreshold * 1024 * 1024))
{
// 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);
_logger?.LogInfo("Image {2} scaled to {0}x{1}", loadedImageWidth, loadedImageHeight, fileName);
}
didAdjust = true;
_logger?.LogInfo("Converted image {0} to JPEG", fileName);
}
else
{
// load height/width
loadedImageWidth = mImage.Width;
loadedImageHeight = mImage.Height;
}
if (didAdjust)
{
await mImage.WriteAsync(convertedOutputPath);
filePath = convertedOutputPath;
_logger?.LogInfo(string.Format("Saved adjusted image to JPEG; file path is now {0}", filePath));
}
// write to PDF
var paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center;
var image = paragraph.AddImage(filePath);
image.Resolution = imageResolution; // dots per inch
image.Tag = "ReceiptImageTag";
paragraph.Tag = "ReceiptImageParagraphTag";
image.LineFormat = imageLineFormat.Clone();
// resize down until it will fit on the page
while (loadedImageHeight > remainingHeightPixels || loadedImageWidth > maxItemPxWidth)
{
// Console.WriteLine("Image height = {0}, width = {1}; decreasing size by 5% to h={2}, w={3}", loadedImageHeight, loadedImageWidth, (uint)Math.Floor(loadedImageHeight * reduceImageSizeAmount), (uint)Math.Floor(loadedImageWidth * reduceImageSizeAmount));
// keep reducing size by 5% (little by little) until it fits on the page
// ...might skew ever so slightly but should not be noticable...
loadedImageHeight = (uint)Math.Floor(loadedImageHeight * reduceImageSizeAmount);
loadedImageWidth = (uint)Math.Floor(loadedImageWidth * reduceImageSizeAmount);
}
image.Height = loadedImageHeight;
image.Width = loadedImageWidth;
}
else // isPDF
{
// need to render PDF to images
if (appSettings.UseDocnetPDFImageRendering)
{
// render using Docnet library (which utilizes pdfium, the chrome renderer)
string RenderPdfPageToImage(IDocReader docReader, int pgNum)
{
Console.WriteLine("Rendering pg " + pgNum);
using var pageReader = docReader.GetPageReader(pgNum);
Console.WriteLine("Getting image for page " + pgNum);
var rawBytes = pageReader.GetImage(RenderFlags.RenderAnnotations);
Console.WriteLine("Getting width & height for page " + pgNum);
var width = pageReader.GetPageWidth();
var height = pageReader.GetPageHeight();
Console.WriteLine("Loading pixel data for page " + pgNum);
using var img = Image.LoadPixelData<Bgra32>(rawBytes, width, height);
// you are likely going to want this as well otherwise you might end up with transparent parts.
img.Mutate(x => x.BackgroundColor(SixLabors.ImageSharp.Color.White));
var pdfPageImageOutputPath = Path.Combine(convertedDir, info.Name + "-Page-"
+ (pgNum + 1).ToString().PadLeft(3, '0') + ".jpg");
img.Save(pdfPageImageOutputPath);
Console.WriteLine("Done rendering pg " + pgNum);
return pdfPageImageOutputPath;
}
// render all pages to images
var docReader = DocLib.Instance.GetDocReader(
filePath,
new PageDimensions(1080, 1920)); // TODO: are these dims right?
// add to document
var pgCount = docReader.GetPageCount();
if (pgCount > 0)
{
var convertedPdfImagePath = RenderPdfPageToImage(docReader, 0);
imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ",
pgCount,
pgCount == 1 ? "" : "s"));
var paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center;
// get image height/width off of disk so we can resize down if needed
var image = paragraph.AddImage(convertedPdfImagePath);
image.LockAspectRatio = true;
image.LineFormat = imageLineFormat.Clone();
using (var firstPdfPageImage = new MagickImage(convertedPdfImagePath))
{
var pdfPageImageWidth = firstPdfPageImage.Width;
var pdfPageImageHeight = firstPdfPageImage.Height;
// resize down until it will fit on the page
while (pdfPageImageHeight > remainingHeightPixels || pdfPageImageWidth > maxItemPxWidth)
{
pdfPageImageHeight = (uint)Math.Floor(pdfPageImageHeight * reduceImageSizeAmount);
pdfPageImageWidth = (uint)Math.Floor(pdfPageImageWidth * reduceImageSizeAmount);
}
image.Height = pdfPageImageHeight;
image.Width = pdfPageImageWidth;
}
for (var j = 1; j < pgCount; j++)
{
section.AddPageBreak();
paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center;
convertedPdfImagePath = RenderPdfPageToImage(docReader, j);
image = paragraph.AddImage(convertedPdfImagePath);
image.LockAspectRatio = true;
image.Width = maxImageWidth;
image.LineFormat = imageLineFormat.Clone();
using (var otherPdfPageImage = new MagickImage(convertedPdfImagePath))
{
var pdfPageImageWidth = otherPdfPageImage.Width;
var pdfPageImageHeight = otherPdfPageImage.Height;
// resize down until it will fit on the page
while (pdfPageImageHeight > remainingHeightPixels || pdfPageImageWidth > maxItemPxWidth)
{
pdfPageImageHeight = (uint)Math.Floor(pdfPageImageHeight * reduceImageSizeAmount);
pdfPageImageWidth = (uint)Math.Floor(pdfPageImageWidth * reduceImageSizeAmount);
}
image.Height = pdfPageImageHeight;
image.Width = pdfPageImageWidth;
}
}
}
}
else
{
// use older, not-docnet rendering method.
// uses MigraDoc rendering. Does not work with annotations, and since Migradoc
// doesn't let us know how big the image is, we can't do the image resizing, so
// we just do our best.
// render first page (eventually need to improve code to just do everything in a loop)
var paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center;
var image = paragraph.AddImage(filePath);
image.LockAspectRatio = true;
image.Width = maxImageWidth; // can't be too wide now...not sure why...maybe due to margins...
image.LineFormat = imageLineFormat.Clone();
// render other PDF pages, if any
// see: https://stackoverflow.com/a/65091204/3938401
var pdfFileToAdd = PdfReader.Open(filePath, PdfDocumentOpenMode.Import);
var pgCount = pdfFileToAdd.PageCount;
imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ",
pgCount,
pgCount == 1 ? "" : "s"));
for (var j = 2; j <= pgCount; j++)
{
section.AddPageBreak();
paragraph = section.AddParagraph();
paragraph.Format.Alignment = ParagraphAlignment.Center;
image = paragraph.AddImage(filePath + "#" + j);
image.LockAspectRatio = true;
image.Width = maxImageWidth;
image.LineFormat = imageLineFormat.Clone();
}
}
}
_logger?.LogInfo(string.Format("Added image: {0} ({1})", file.Title, filePath));
hasAddedData = true;
}
_logger?.LogInfo("Rendering document to PDF file...");
pdfRenderer.DocumentRenderer.PrepareDocument(); // needed if you make edits after first PrepareDocument() is called
pdfRenderer.RenderDocument();
// actually save to disk now
_logger?.LogInfo("Saving PDF document to disk...");
pdfRenderer.PdfDocument.Save(outputFilePathWithName);
_logger?.LogInfo("Finished saving PDF output to: " + outputFilePathWithName);
// clean up converted files data dir
Directory.Delete(convertedDir, true);
// return output path
return outputFilePathWithName;
}
}
+331
View File
@@ -0,0 +1,331 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using MayShow.Enums;
using MayShow.Helpers;
namespace MayShow.Models;
class Settings : ChangeNotifier
{
private string _lastUsedPath;
private bool _useDocnetPDFImageRendering;
private bool _saveOutputPdfInWorkingDir; // obsolete
private string _outputPdfDir;
private decimal _imageResizeThreshold;
private Dictionary<string, string> _workingFolderToInternalFolderName; // obsolete
private List<PDFReportInfo> _allReportInfo;
public string _dataGridDateFormat;
public string _reportDateFormat;
public int _settingsVersion;
private PDFSaveLocation _pdfOutputSaveLocation;
public bool _copyFilesToInternalDir;
public Settings() : base()
{
_lastUsedPath = "";
_useDocnetPDFImageRendering = true;
_saveOutputPdfInWorkingDir = true;
_outputPdfDir = "";
_imageResizeThreshold = 1.5m;
_workingFolderToInternalFolderName = [];
_allReportInfo = [];
_settingsVersion = 3;
_dataGridDateFormat = "dd/MM/yyyy";
_reportDateFormat = "yyyy-MM-dd";
_pdfOutputSaveLocation = PDFSaveLocation.BaseFolder;
_copyFilesToInternalDir = false;
#if IOS
_copyFilesToInternalDir = true;
#endif
}
public Settings(Settings other)
{
_lastUsedPath = other.LastUsedPath;
_useDocnetPDFImageRendering = other.UseDocnetPDFImageRendering;
_saveOutputPdfInWorkingDir = other.SaveOutputPdfInWorkingDir;
_outputPdfDir = other.OutputPdfDir;
_imageResizeThreshold = other.ImageResizeThreshold;
_workingFolderToInternalFolderName = other.WorkingFolderToInternalFolderName;
_settingsVersion = other.SettingsVersion;
_allReportInfo = other.AllReportInfo;
_dataGridDateFormat = other.DataGridDateFormat;
_reportDateFormat = other.ReportDateFormat;
_pdfOutputSaveLocation = other.PDFOutputSaveLocation;
_copyFilesToInternalDir = other.CopyFilesToInternalDir;
}
[JsonInclude]
public string LastUsedPath
{
get => _lastUsedPath;
set { _lastUsedPath = value; NotifyPropertyChanged(); }
}
[JsonInclude]
[JsonPropertyName("UseDocnetPFDImageRendering")] // ...this typo now has to live because people have this saved on disk...
public bool UseDocnetPDFImageRendering
{
get => _useDocnetPDFImageRendering;
set { _useDocnetPDFImageRendering = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public bool SaveOutputPdfInWorkingDir
{
get => _saveOutputPdfInWorkingDir;
set { _saveOutputPdfInWorkingDir = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public PDFSaveLocation PDFOutputSaveLocation
{
get => _pdfOutputSaveLocation;
set { _pdfOutputSaveLocation = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string OutputPdfDir
{
get => _outputPdfDir;
set { _outputPdfDir = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public decimal ImageResizeThreshold
{
get => _imageResizeThreshold;
set { _imageResizeThreshold = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public Dictionary<string, string> WorkingFolderToInternalFolderName
{
get => _workingFolderToInternalFolderName;
set { _workingFolderToInternalFolderName = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public List<PDFReportInfo> AllReportInfo
{
get => _allReportInfo;
set { _allReportInfo = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public int SettingsVersion
{
get => _settingsVersion;
set { _settingsVersion = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string DataGridDateFormat
{
get => _dataGridDateFormat;
set { _dataGridDateFormat = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string ReportDateFormat
{
get => _reportDateFormat;
set { _reportDateFormat = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public bool CopyFilesToInternalDir
{
get => _copyFilesToInternalDir;
set { _copyFilesToInternalDir = value; NotifyPropertyChanged(); }
}
public static string SettingsFileName = "settings.json";
public static string GetSettingsPath()
{
var path = Utilities.GetInternalDataPath();
return Path.Combine(path, SettingsFileName);
}
public string SaveSettingsNotAsync()
{
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
using MemoryStream memoryStream = new MemoryStream();
JsonSerializer.Serialize(memoryStream, this, jsonContext.Settings);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var json = reader.ReadToEnd();
File.WriteAllText(GetSettingsPath(), json);
return json;
}
public async Task<string> SaveSettingsAsync()
{
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
using MemoryStream memoryStream = new MemoryStream();
await JsonSerializer.SerializeAsync(memoryStream, this, jsonContext.Settings);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var json = await reader.ReadToEndAsync();
await File.WriteAllTextAsync(GetSettingsPath(), json);
return json;
}
private static Settings UpgradeSettings(Settings settings)
{
if (settings.SettingsVersion == 1)
{
// update settings
var internalPath = Utilities.GetInternalDataPath();
var list = new List<PDFReportInfo>();
foreach (var data in settings.WorkingFolderToInternalFolderName)
{
var uuid = data.Value;
var path = Path.Combine(internalPath, uuid, Constants.ReportSavedDataFileName);
var json = File.ReadAllText(path);
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
var report = File.Exists(path) ? JsonSerializer.Deserialize(json, jsonContext.PDFReport) : null;
var reportTitle = report?.Title ?? "";
var lastSaved = report?.LastSaved;
var reportInfo = new PDFReportInfo()
{
Title = reportTitle,
UUID = uuid,
LastSaved = lastSaved,
BaseFolder = data.Key,
};
// sync UUIDs
// if UUID exists in BaseFolder/(Constants.ReportSavedDataFileName), use that UUID instead.
var externalReportDataPath = Path.Combine(reportInfo.BaseFolder, Constants.ReportSavedDataFileName);
if (File.Exists(externalReportDataPath))
{
var originalReportData = JsonSerializer.Deserialize(File.ReadAllText(externalReportDataPath), jsonContext.PDFReport);
if (originalReportData != null)
{
if (!string.IsNullOrWhiteSpace(originalReportData.UUID))
{
Directory.Move(Path.Combine(internalPath, uuid), Path.Combine(internalPath, originalReportData.UUID));
reportInfo.UUID = originalReportData.UUID;
}
else
{
// update UUID so they are in sync between internal and external folders
originalReportData.UUID = reportInfo.UUID;
Utilities.SaveReportDataSync(originalReportData, externalReportDataPath, jsonContext.PDFReport);
}
}
}
// update report data itself and move to internal -- everything is moving to internal storage dir,
// so if there is external data, use whatever is the most recent.
// reportInfo.UUID now has the UUID we want to use.
var internalReportFolderPath = Path.Combine(internalPath, reportInfo.UUID);
var internalDataFilePath = Path.Combine(internalReportFolderPath, Constants.ReportSavedDataFileName);
if (!Path.Exists(internalReportFolderPath))
{
// internal path doesn't exist at all so never saved internally before.
// make the dir and copy data to internal dir.
Directory.CreateDirectory(internalReportFolderPath);
if (File.Exists(externalReportDataPath))
{
File.Copy(externalReportDataPath, Path.Combine(internalReportFolderPath, Constants.ReportSavedDataFileName));
}
}
else
{
// see which JSON file is newer (based on last saved time) and use that data.
if (!File.Exists(internalDataFilePath))
{
// internal file doesn't exist, copy in from external
if (File.Exists(externalReportDataPath))
{
File.Copy(externalReportDataPath, internalDataFilePath);
}
}
else if (File.Exists(internalDataFilePath) && File.Exists(externalReportDataPath))
{
// both files exist. load report data and compare dates.
var internalReportData = JsonSerializer.Deserialize(File.ReadAllText(internalDataFilePath), jsonContext.PDFReport);
var externalReportData = JsonSerializer.Deserialize(File.ReadAllText(externalReportDataPath), jsonContext.PDFReport);
if (internalReportData != null && externalReportData != null)
{
var isExternalNewer = (externalReportData.LastSaved ?? DateTime.MinValue)
> (internalReportData.LastSaved ?? DateTime.MinValue);
if (isExternalNewer) // else internal is newer so nothing to do
{
File.Move(internalDataFilePath, Path.Combine(internalReportFolderPath, "old_report_data.json"));
File.Copy(externalReportDataPath, internalDataFilePath, true);
reportInfo.Title = externalReportData.Title;
reportInfo.LastSaved = externalReportData.LastSaved;
}
}
else if (internalReportData == null && externalReportData != null)
{
// move data to internal dir
if (File.Exists(externalReportDataPath))
{
File.Copy(externalReportDataPath, internalDataFilePath, true);
}
}
}
}
reportInfo.BaseFolder = internalReportFolderPath;
// make sure BaseFolder is set right just in case -- now always points to internal directory.
// (it's actually now redundant because all settings are internal...
// but for now we'll just let it stick around.)
if (File.Exists(internalDataFilePath))
{
var internalReportData = JsonSerializer.Deserialize(File.ReadAllText(internalDataFilePath), jsonContext.PDFReport);
if (internalReportData != null)
{
internalReportData.BaseFolder = internalReportFolderPath;
Utilities.SaveReportDataSync(internalReportData, internalDataFilePath, jsonContext.PDFReport);
}
}
// ok, finally done upgrading this report.
list.Add(reportInfo);
}
settings.AllReportInfo = list.OrderBy(x => x.Title).ToList();
settings.WorkingFolderToInternalFolderName = []; // clear this list; it is no longer going to be used
settings.SettingsVersion = 2;
settings.SaveSettingsNotAsync(); // saves all data; UUIDs should be in sync if user has toggled settings
}
if (settings.SettingsVersion == 2)
{
if (!settings.SaveOutputPdfInWorkingDir)
{
settings.PDFOutputSaveLocation = PDFSaveLocation.OtherChosenDir;
}
settings.SettingsVersion = 3;
settings.SaveSettingsNotAsync();
}
return settings;
}
public static Settings LoadSettings()
{
var path = GetSettingsPath();
if (!File.Exists(path))
{
return new Settings();
}
var json = File.ReadAllText(GetSettingsPath());
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
return UpgradeSettings(JsonSerializer.Deserialize<Settings>(json, jsonContext.Settings) ?? new Settings());
}
public static async Task<Settings> LoadSettingsAsync()
{
using FileStream fileStream = File.OpenRead(GetSettingsPath());
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
var output = await JsonSerializer.DeserializeAsync<Settings>(fileStream, jsonContext.Settings) ?? new Settings();
return UpgradeSettings(output);
}
}
@@ -0,0 +1,15 @@
using DialogHostAvalonia;
namespace MayShow.ViewModels;
class AboutViewModel
{
public AboutViewModel()
{
}
public void Close()
{
DialogHost.Close("DialogHost", null);
}
}
@@ -1,9 +1,5 @@
using Avalonia.Controls; using MayShow.Helpers;
using MayShow.Helpers;
using MayShow.Interfaces; using MayShow.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace MayShow.ViewModels; namespace MayShow.ViewModels;
@@ -12,7 +8,7 @@ class BaseViewModel : ChangeNotifier
IChangeViewModel _viewModelChanger; IChangeViewModel _viewModelChanger;
ITopLevelGrabber? _topLevelGrabber; ITopLevelGrabber? _topLevelGrabber;
public BaseViewModel(IChangeViewModel viewModelChanger) public BaseViewModel(IChangeViewModel viewModelChanger): base()
{ {
_viewModelChanger = viewModelChanger; _viewModelChanger = viewModelChanger;
_topLevelGrabber = null; _topLevelGrabber = null;
@@ -1,16 +1,16 @@
#nullable enable
using DialogHostAvalonia; using DialogHostAvalonia;
using MayShow.Helpers; using MayShow.Helpers;
namespace MayShow.ViewModels; namespace MayShow.ViewModels;
class ConfirmViewModel class ConfirmViewModel : ChangeNotifier
{ {
private string _title; private string _title;
private string _message; private string _message;
private string _confirmTitle; private string _confirmTitle;
private string _declineTitle; private string _declineTitle;
private bool _confirmButtonUsesDangerStyle;
private string _confirmButtonIcon;
public ConfirmViewModel(string title, string message, string confirmTitle = "Yes", string declineTitle = "No") public ConfirmViewModel(string title, string message, string confirmTitle = "Yes", string declineTitle = "No")
{ {
@@ -18,6 +18,8 @@ class ConfirmViewModel
_message = message; _message = message;
_confirmTitle = confirmTitle; _confirmTitle = confirmTitle;
_declineTitle = declineTitle; _declineTitle = declineTitle;
_confirmButtonUsesDangerStyle = false;
_confirmButtonIcon = "";
} }
public string Title public string Title
@@ -40,6 +42,35 @@ class ConfirmViewModel
get => _declineTitle; get => _declineTitle;
} }
public bool ConfirmButtonIsAccent
{
get => !_confirmButtonUsesDangerStyle;
}
public bool ConfirmButtonIsDanger
{
get => _confirmButtonUsesDangerStyle;
}
public bool ConfirmButtonUsesDangerStyle
{
set
{
_confirmButtonUsesDangerStyle = value;
NotifyPropertyChanged(nameof(ConfirmButtonIsAccent));
NotifyPropertyChanged(nameof(ConfirmButtonIsDanger));
}
}
public string ConfirmTitleIcon
{
get => _confirmButtonIcon;
set
{
_confirmButtonIcon = value; NotifyPropertyChanged();
}
}
public void Confirm() public void Confirm()
{ {
DialogHost.Close("DialogHost", true); DialogHost.Close("DialogHost", true);
@@ -0,0 +1,649 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using DialogHostAvalonia;
using MayShow.Helpers;
using MayShow.Interfaces;
using MayShow.Models;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Threading;
using MayShow.Enums;
namespace MayShow.ViewModels;
class CreatePDFReportViewModel : BaseViewModel, ICanCheckShutdown, ILogger
{
#pragma warning disable CS0414
private bool _isPerformingInitialLoad;
#pragma warning restore CS0414
private string _processDir;
private string _programLog;
private bool _isCreatingPDF;
private PDFReport _pdfReport;
private Settings _settings;
private List<DateDisplayFormat> _dateDisplayFormats;
private bool _hasUnsavedWork;
private CreatePDFReportViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
{
_pdfReport = new PDFReport();
_processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
Console.WriteLine("Internal storage directory is: {0}", Utilities.GetInternalDataPath());
_isCreatingPDF = false;
ReportFiles = [];
_programLog = "";
_settings = Settings.LoadSettings();
_dateDisplayFormats = Constants.GetDateDisplayFormats();
NotifyPropertyChanged(nameof(DataGridDateFormat));
NotifyPropertyChanged(nameof(DataGridDateFormatWatermark));
HasUnsavedWork = false;
// setup initial quote and program log data
InitializeProgramLog();
}
public CreatePDFReportViewModel(PDFReportInfo reportInfo, IChangeViewModel viewModelChanger) : this(viewModelChanger)
{
_isPerformingInitialLoad = true;
_pdfReport = new PDFReport(reportInfo);
// always default to using BaseFolder, which will always be set in the general case
if (!string.IsNullOrWhiteSpace(_pdfReport.BaseFolder))
{
LogInfo("Loading report data at path: {0}", _pdfReport.BaseFolder);
ScanFolder(_pdfReport.BaseFolder);
}
else
{
// load data file in internal data report dir
_pdfReport.BaseFolder = Path.Combine(Utilities.GetInternalDataPath(), _pdfReport.UUID);
if (Directory.Exists(_pdfReport.BaseFolder))
{
ScanFolder(_pdfReport.BaseFolder); // even if points entirely to internal folder, we will be A-OK loading here
}
else
{
LogInfo("Erorr loading report! Folder does not exist: {0}", _pdfReport.BaseFolder);
}
}
_isPerformingInitialLoad = false;
}
public IUpdateRecentlyUsed? UpdateRecentlyUsed { get; set; }
public PDFReport PDFReport
{
get => _pdfReport;
set
{
_pdfReport = value;
NotifyPropertyChanged(nameof(ReportTitle));
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
NotifyPropertyChanged(nameof(ReportFiles));
SetupFileCollectionChangedWatcher();
}
}
public string ReportTitle
{
get => _pdfReport.Title;
set
{
_pdfReport.Title = value;
NotifyPropertyChanged();
HasUnsavedWork = true;
}
}
public bool CanAddItem
{
get => !IsCreatingPDF;
}
public bool IsCreatingPDF
{
get => _isCreatingPDF;
set
{
_isCreatingPDF = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
NotifyPropertyChanged(nameof(CanAddItem));
NotifyPropertyChanged(nameof(IsSaveButtonAccentOn));
}
}
public bool IsSaveButtonAccentOn
{
get => !_isCreatingPDF && HasUnsavedWork;
}
public bool IsCreatePDFButtonEnabled
{
get => !_isCreatingPDF && _pdfReport.Files.Count > 0;
}
public string ProgramLog
{
get => _programLog;
set { _programLog = value; NotifyPropertyChanged(); }
}
public bool HasUnsavedWork
{
get => _hasUnsavedWork;
set
{
_hasUnsavedWork = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsSaveButtonAccentOn));
}
}
public ObservableCollection<ReportFile> ReportFiles
{
get => _pdfReport.Files;
set
{
_pdfReport.Files = value;
NotifyPropertyChanged();
SetupFileCollectionChangedWatcher();
}
}
public string DataGridDateFormat
{
get => _settings.DataGridDateFormat;
}
public string DataGridDateFormatWatermark
{
get => _dateDisplayFormats.FirstOrDefault(x => x.Value == _settings.DataGridDateFormat)?.Example ?? "2025-12-04";
}
private void SetupFileCollectionChangedWatcher()
{
_pdfReport.Files.CollectionChanged += ( sender, e ) =>
{
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
HasUnsavedWork = true;
};
}
private void InitializeProgramLog()
{
var quotes = Constants.GetQuotes();
var random = new Random();
var quoteIndex = random.Next(0, quotes.Length);
var compDetails = RuntimeInformation.OSDescription + " | " +
RuntimeInformation.OSArchitecture.ToString();
_programLog = "----- MayShow v" + Constants.AppVersion + " | " + compDetails + " ------" + Environment.NewLine;
_programLog += quotes[quoteIndex] + Environment.NewLine;
_programLog += "---------------------------------------" + Environment.NewLine;
_programLog += "Loaded and ready to create report!" + Environment.NewLine;
_programLog += "Please copy and send this Program Log when reporting any issues with the software.";
}
public void LogInfo(string message, params object[]? arguments)
{
var timestamp = string.Format("[{0:s}]", DateTime.Now);
Console.WriteLine(timestamp + " " + message, arguments);
ProgramLog += Environment.NewLine + string.Format(message, arguments ?? []);
}
public async void ChooseFolder()
{
var topLevel = TopLevelGrabber?.GetTopLevel();
if (topLevel is not null)
{
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()
{
Title = "Pick a folder of files...",
AllowMultiple = false,
});
if (folders.Count == 1)
{
var folder = folders[0];
LogInfo("Clearing existing list and loading items in folder: " + folder.Path.LocalPath);
ReportFiles.Clear();
ScanFolder(folder.Path.LocalPath);
_settings.LastUsedPath = folder.Path.LocalPath;
await _settings.SaveSettingsAsync();
ResortPDFItemsByDate();
HasUnsavedWork = true;
}
}
}
private string GetReportSavedDataPath()
{
if (!Directory.Exists(_pdfReport.BaseFolder))
{
Directory.CreateDirectory(_pdfReport.BaseFolder);
}
return Path.Combine(_pdfReport.BaseFolder, Constants.ReportSavedDataFileName);
}
private void ScanFolder(string path)
{
if (Directory.Exists(path))
{
var reportFilePath = GetReportSavedDataPath();
var successfullyLoadedPriorReportFile = false;
if (File.Exists(reportFilePath))
{
// load prior report
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
var report = JsonSerializer.Deserialize(File.ReadAllText(reportFilePath), jsonContext.PDFReport);
if (report != null)
{
PDFReport = report;
Console.WriteLine("Loading prior report data at {0}", reportFilePath);
LogInfo("Reloaded report last saved at {0}", report.LastSaved ?? DateTime.Now);
successfullyLoadedPriorReportFile = true;
}
}
if (!successfullyLoadedPriorReportFile)
{
// Scan folder for files and display in DataGrid
if (path != PDFReport.BaseFolder)
{
// in this case, there is essentially no existing report,
// so we need to make a new one.
PDFReport = new PDFReport()
{
Title = Path.GetDirectoryName(path) ?? "",
LastSaved = null,
UUID = Utilities.GetUniqueReportGuid(_settings).ToString(),
};
PDFReport.UpdateBaseFolder();
}
ReportFiles.Clear();
ReportTitle = "";
var filePaths = Directory.GetFiles(path);
foreach (var filePath in filePaths)
{
AddFileBasedOnPath(filePath);
}
ResortPDFItemsByDate();
HasUnsavedWork = true;
}
}
else
{
LogInfo("Error: The directory {0} does not exist. Please select another folder.", path);
}
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
}
// https://github.com/AvaloniaUI/Avalonia/issues/10075
public void EditFileProperties(object f) => EditFilePropertiesImpl((ReportFile)f);
public async void EditFilePropertiesImpl(ReportFile file)
{
var result = await DialogHost.Show(new EditFileViewModel(file, ViewModelChanger));
if (result != null && result is ReportFile updatedData)
{
file.Title = updatedData.Title;
file.ReceiptDateTime = updatedData.ReceiptDateTime;
file.Notes = updatedData.Notes;
HasUnsavedWork = true;
}
}
public async void AddItem()
{
var topLevel = TopLevelGrabber?.GetTopLevel();
if (topLevel is not null)
{
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Choose image or PDF files...",
AllowMultiple = true,
FileTypeFilter = Utilities.GetReportFilePickerFileTypes(),
});
if (files.Count > 0)
{
foreach (var file in files)
{
var filePath = file.TryGetLocalPath();
AddFileBasedOnPath(filePath);
}
}
}
}
private void AddFileBasedOnPath(string? filePath)
{
if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath) && !filePath.EndsWith(".DS_Store"))
{
// make sure extensions are OK
var fileExtensions = Constants.AllowedFileExtensionsNoStar;
var didMatch = false;
foreach (var fileExtension in fileExtensions)
{
if (filePath.ToLower().EndsWith("." + fileExtension.ToLower()))
{
didMatch = true;
break;
}
}
if (!didMatch)
{
if (!filePath.EndsWith(Constants.ReportSavedDataFileName))
{
LogInfo("File {0} did not match allowed file extension types, so it was not added.", filePath);
}
}
else
{
var date = Utilities.CheckValidDateInString(filePath);
if (_settings.CopyFilesToInternalDir)
{
// copy file to internal folder, then add report file based on that path
// make sure file names are not conflicting with one another, too.
var fileName = Path.GetFileName(filePath);
var fileNameNoExt = Path.GetFileNameWithoutExtension(filePath);
var extension = Path.GetExtension(filePath);
var copyToPath = Path.Combine(_pdfReport.BaseFolder, fileName);
var rnd = new Random();
// TODO: test to make sure this works
while (File.Exists(copyToPath))
{
fileName = fileNameNoExt + rnd.Next(1, 999999) + "." + extension;
copyToPath = Path.Combine(_pdfReport.BaseFolder, fileName);
}
File.Copy(filePath, copyToPath);
filePath = copyToPath;
}
ReportFiles.Add(new ReportFile()
{
Title = Path.GetFileName(filePath),
ReceiptDateTime = date.HasValue ? date.Value.ToDateTime(TimeOnly.MinValue) : File.GetCreationTime(filePath),
Notes = "",
FilePath = filePath,
});
HasUnsavedWork = true;
}
}
}
public void RemoveFile(object f) => RemoveFileImpl((ReportFile)f);
public async void RemoveFileImpl(ReportFile file)
{
var result = await DialogHost.Show(new WarningDeleteItemViewModel(file));
if (result != null && (bool)result)
{
var idx = ReportFiles.IndexOf(file);
if (idx != -1)
{
ReportFiles.RemoveAt(idx);
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")
{
ConfirmButtonUsesDangerStyle = true,
ConfirmTitleIcon = "\uf1f8;"
});
if (result != null && (bool)result)
{
ReportFiles.Clear();
HasUnsavedWork = true;
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
}
}
public void LocateFile(object f) => LocateFileImpl((ReportFile)f);
public async void LocateFileImpl(ReportFile reportFile)
{
var topLevel = TopLevelGrabber?.GetTopLevel();
if (topLevel is not null)
{
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Choose image or PDF file...",
AllowMultiple = false,
FileTypeFilter = Utilities.GetReportFilePickerFileTypes(),
});
if (files.Count > 0)
{
var file = files[0];
reportFile.FilePath = file.Path.LocalPath;
HasUnsavedWork = true;
}
}
}
// https://github.com/AvaloniaUI/Avalonia/issues/10075
public void OpenFile(object f) => OpenFileImpl((ReportFile)f);
public void OpenFileImpl(ReportFile file)
{
var topLevel = TopLevelGrabber?.GetTopLevel();
if (topLevel is not null)
{
var launcher = topLevel.Launcher;
launcher.LaunchUriAsync(new Uri(file.FilePath));
}
}
public void OpenFileLocation(object f) => OpenFileLocationImpl((ReportFile)f);
private void OpenFileLocationImpl(ReportFile file)
{
OpenFolderForFileInFileViewer(file.FilePath);
}
private void OpenFolderForFileInFileViewer(string fullPathToFile)
{
var topLevel = TopLevelGrabber?.GetTopLevel();
var dirName = Path.GetDirectoryName(fullPathToFile);
if (topLevel is not null && dirName != null)
{
var launcher = topLevel.Launcher;
launcher.LaunchUriAsync(new Uri(dirName));
}
}
public void ResortPDFItemsByDate()
{
LogInfo("Sorting report files list...");
ReportFiles = new ObservableCollection<ReportFile>(
ReportFiles.OrderBy(x => x.ReceiptDateTime)
.ThenBy(x => x.Title));
HasUnsavedWork = true;
}
// called from UI button
public async void BuildPDF()
{
if (string.IsNullOrWhiteSpace(ReportTitle))
{
await DialogHost.Show(new WarningViewModel("You must provide a report title!"));
}
else
{
try
{
var outputFilePath = await DeterminePDFSaveLocation();
if (outputFilePath == null && _settings.PDFOutputSaveLocation != PDFSaveLocation.AlwaysAsk)
{
// if always ask and output is null, they probably hit cancel.
await DialogHost.Show(new WarningViewModel("Error: Output file path could not be determined. Current save location set in settings: " + Enum.GetName(_settings.PDFOutputSaveLocation)));
}
else if (_settings.PDFOutputSaveLocation == PDFSaveLocation.OtherChosenDir &&
!Directory.Exists(_settings.OutputPdfDir))
{
await DialogHost.Show(new WarningViewModel("Error: Output directory not found! Please adjust the application Settings before continuing. Output directory: " + _settings.OutputPdfDir));
}
else if (outputFilePath != null)
{
await Task.Run(() => CreatePDF(outputFilePath));
}
} catch (Exception e)
{
LogInfo("PDF process failed! Reason: " + e.Message);
if (e.StackTrace != null)
{
LogInfo(e.StackTrace);
}
var otherException = e.InnerException;
while (otherException != null)
{
LogInfo(">> Inner exception: " + otherException.Message);
if (otherException.StackTrace != null)
{
LogInfo(otherException.StackTrace);
}
otherException = otherException.InnerException;
}
LogInfo("Please report this error to a programmer or fix the issue listed above.");
IsCreatingPDF = false;
}
}
}
public async Task SaveInterimReportInfo()
{
_pdfReport.LastSaved = DateTime.Now;
await SavePDFReportDataToDisk(_pdfReport);
}
private async Task CreateAndSaveReportObjectAfterReportCreation()
{
_pdfReport.LastSaved = DateTime.Now;
_pdfReport.LastGenerated = DateTime.Now;
await SavePDFReportDataToDisk(_pdfReport);
}
private async Task SavePDFReportDataToDisk(PDFReport report)
{
var savePath = GetReportSavedDataPath();
await Utilities.SaveReportDataAsync(report, savePath);
LogInfo("Saved report information to {0}", savePath);
HasUnsavedWork = false;
UpdateRecentlyUsed?.UpdateRecentlyUsed(report);
}
// called from UI button
public async Task CopyLogToClipboard()
{
var clipboard = TopLevelGrabber?.GetTopLevel().Clipboard;
if (clipboard != null)
{
await clipboard.SetTextAsync(ProgramLog);
LogInfo("Program log has been copied to the clipboard!");
}
}
private async Task<string?> DeterminePDFSaveLocation()
{
var fileName = ReportTitle + ".pdf";
switch (_settings.PDFOutputSaveLocation)
{
case PDFSaveLocation.BaseFolder:
return Path.Combine(_pdfReport.BaseFolder, fileName);
case PDFSaveLocation.AlwaysAsk:
Func<Task<string?>> getSaveFilePath = async () =>
{
var topLevel = TopLevelGrabber?.GetTopLevel();
if (topLevel != null)
{
var result = await topLevel.StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
{
Title = "Choose PDF save location...",
FileTypeChoices = [FilePickerFileTypes.Pdf],
SuggestedFileType = FilePickerFileTypes.Pdf,
DefaultExtension = "pdf",
ShowOverwritePrompt = true,
SuggestedFileName = ReportTitle + ".pdf"
});
if (result.File != null)
{
// Console.WriteLine("1: {0}", result.File.Path.AbsolutePath); // HTML escaped?!
// Console.WriteLine("2: {0}", result.File.Path.AbsoluteUri); // starts with file://
// Console.WriteLine("3: {0}", result.File.Path.LocalPath); // path for OS
// Console.WriteLine("4: {0}", result.File.Path.OriginalString); // starts with file://
// Console.WriteLine("5: {0}", result.File.Path.UserEscaped); // bool
// Console.WriteLine("6: {0}", result.File.Name); // just file name, no path
var path = result.File.Path.LocalPath;
if (!path.EndsWith(".pdf"))
{
// should be fine, but juuuust in case...
path += ".pdf";
}
// Console.WriteLine(path);
return path;
}
}
return null;
};
// must invoke on UI thread because getting file picker
return await Dispatcher.UIThread.InvokeAsync(getSaveFilePath);
case PDFSaveLocation.OtherChosenDir:
return Path.Combine(_settings.OutputPdfDir, fileName);
}
return null;
}
private async Task CreatePDF(string outputFilePath)
{
IsCreatingPDF = true;
var reportCreator = new ReportPDFCreator(this);
var outputPdfFile = await reportCreator.CreatePDF(ReportFiles.ToList(), ReportTitle, outputFilePath, new PDFFontResolver(_processDir, this), _settings);
if (!string.IsNullOrWhiteSpace(outputPdfFile))
{
await CreateAndSaveReportObjectAfterReportCreation();
OpenFolderForFileInFileViewer(outputPdfFile);
}
IsCreatingPDF = false;
}
public async void ReturnToMainMenu()
{
if (await CheckIsSafeToShutdown())
{
PopViewModel();
}
}
public async Task<bool> CheckIsSafeToShutdown()
{
if (!HasUnsavedWork)
{
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;
}
}
@@ -1,20 +1,18 @@
using MayShow.Helpers; using MayShow.Helpers;
using MayShow.Interfaces; using MayShow.Interfaces;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace MayShow.ViewModels; namespace MayShow.ViewModels;
class MainWindowViewModel : ChangeNotifier, IChangeViewModel class MainViewModel : ChangeNotifier, IChangeViewModel
{ {
BaseViewModel _currentViewModel; BaseViewModel _currentViewModel;
Stack<BaseViewModel> _viewModels; Stack<BaseViewModel> _viewModels;
public MainWindowViewModel(ITopLevelGrabber topLevelGrabber) public MainViewModel(ITopLevelGrabber topLevelGrabber): base()
{ {
_viewModels = new Stack<BaseViewModel>(); _viewModels = new Stack<BaseViewModel>();
var initialViewModel = new MainViewModel(this) var initialViewModel = new StartNewChooseReportViewModel(this)
{ {
TopLevelGrabber = topLevelGrabber TopLevelGrabber = topLevelGrabber
}; };
@@ -1,24 +1,14 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Themes.Fluent;
using DialogHostAvalonia; using DialogHostAvalonia;
using ImageMagick;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using PdfSharp.Fonts;
using PdfSharp.Pdf.IO;
using PdfSharp.Snippets.Font;
using MayShow.Interfaces; using MayShow.Interfaces;
using MayShow.Models; using MayShow.Models;
using MayShow.Helpers; using MayShow.Helpers;
using System.Collections.Generic; using System.Collections.Generic;
using MayShow.Enums;
namespace MayShow.ViewModels; namespace MayShow.ViewModels;
@@ -32,7 +22,7 @@ class SettingsViewModel: ChangeNotifier
private int _gridDisplayDateFormatSelectedIndex; private int _gridDisplayDateFormatSelectedIndex;
private int _reportDisplayDateFormatSelectedIndex; private int _reportDisplayDateFormatSelectedIndex;
public SettingsViewModel(Settings settingsToEdit, ITopLevelGrabber? topLevelGrabber) public SettingsViewModel(Settings settingsToEdit, ITopLevelGrabber? topLevelGrabber): base()
{ {
_previousSettings = settingsToEdit; _previousSettings = settingsToEdit;
_settings = new Settings(settingsToEdit); // clone it _settings = new Settings(settingsToEdit); // clone it
@@ -61,13 +51,19 @@ class SettingsViewModel: ChangeNotifier
} }
} }
public bool SaveOutputPdfInWorkingDir public bool SaveOutputPdfInChosenDir
{ {
get => _settings.SaveOutputPdfInWorkingDir; get => PDFOutputSaveLocation == PDFSaveLocation.OtherChosenDir;
}
public PDFSaveLocation PDFOutputSaveLocation
{
get => _settings.PDFOutputSaveLocation;
set set
{ {
_settings.SaveOutputPdfInWorkingDir = value; _settings.PDFOutputSaveLocation = value;
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyPropertyChanged(nameof(SaveOutputPdfInChosenDir));
} }
} }
@@ -84,7 +80,7 @@ class SettingsViewModel: ChangeNotifier
public bool IsOutputPdfDirValid public bool IsOutputPdfDirValid
{ {
get => SaveOutputPdfInWorkingDir || (!SaveOutputPdfInWorkingDir && Directory.Exists(OutputPdfDirPath)); get => !SaveOutputPdfInChosenDir || (SaveOutputPdfInChosenDir && Directory.Exists(OutputPdfDirPath));
} }
public bool HasErrorMessage public bool HasErrorMessage
@@ -113,16 +109,6 @@ class SettingsViewModel: ChangeNotifier
} }
} }
public bool SaveReportJsonDataInInternalDir
{
get => _settings.SaveReportJsonDataInInternalDir;
set
{
_settings.SaveReportJsonDataInInternalDir = value;
NotifyPropertyChanged();
}
}
public List<DateDisplayFormat> DateFormats public List<DateDisplayFormat> DateFormats
{ {
get => _dateFormats; get => _dateFormats;
@@ -171,7 +157,6 @@ class SettingsViewModel: ChangeNotifier
public void OpenSettingsDir() public void OpenSettingsDir()
{ {
var topLevel = _topLevelGrabber?.GetTopLevel(); var topLevel = _topLevelGrabber?.GetTopLevel();
Console.WriteLine(Utilities.GetInternalDataPath());
var dirName = Utilities.GetInternalDataPath(); var dirName = Utilities.GetInternalDataPath();
if (topLevel is not null && dirName != null) if (topLevel is not null && dirName != null)
{ {
@@ -0,0 +1,142 @@
#nullable enable
using System.Collections.ObjectModel;
using System.Linq;
using DialogHostAvalonia;
using MayShow.Interfaces;
using MayShow.Models;
using MayShow.Helpers;
using System.Threading.Tasks;
namespace MayShow.ViewModels;
class StartNewChooseReportViewModel : BaseViewModel, ICanCheckShutdown, IUpdateRecentlyUsed
{
private string _creatingReportTitle;
private ObservableCollection<PDFReportInfo> _savedReports;
private Settings _settings;
public StartNewChooseReportViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
{
_creatingReportTitle = "";
_settings = Settings.LoadSettings();
_savedReports = new ObservableCollection<PDFReportInfo>(_settings.AllReportInfo.OrderBy(x => x.Title));
}
public static string Version
{
get => Constants.AppVersion;
}
public string CreatingReportTitle
{
get => _creatingReportTitle;
set { _creatingReportTitle = value; NotifyPropertyChanged(); }
}
public ObservableCollection<PDFReportInfo> SavedReports
{
get => _savedReports;
set { _savedReports = value; NotifyPropertyChanged(); }
}
public async void StartReport() // start a new report based on a title alone
{
if (string.IsNullOrWhiteSpace(CreatingReportTitle))
{
await DialogHost.Show(new WarningViewModel("Report title cannot be blank!"));
return;
}
var reportInfo = new PDFReportInfo()
{
Title = CreatingReportTitle,
LastSaved = null,
UUID = Utilities.GetUniqueReportGuid(_settings).ToString()
};
reportInfo.UpdateBaseFolder();
// now update UI
ViewModelChanger.PushViewModel(new CreatePDFReportViewModel(reportInfo, ViewModelChanger)
{
UpdateRecentlyUsed = this,
TopLevelGrabber = TopLevelGrabber
});
CreatingReportTitle = ""; // when user comes back they can start another new report
}
public void LoadExistingReport(object info) => LoadExistingReportImpl((PDFReportInfo) info);
public void LoadExistingReportImpl(PDFReportInfo reportInfo)
{
ViewModelChanger.PushViewModel(new CreatePDFReportViewModel(reportInfo, ViewModelChanger)
{
UpdateRecentlyUsed = this,
TopLevelGrabber = TopLevelGrabber
});
}
public void DeleteExistingReport(object info) => DeleteExistingReportImpl((PDFReportInfo) info);
public async void DeleteExistingReportImpl(PDFReportInfo reportInfo)
{
var message = string.IsNullOrWhiteSpace(reportInfo.BaseFolder)
? "Are you sure you want to delete this report and its associated data? It will be gone forever!"
: "Are you sure you want to delete information about this report? It will be gone forever!";
var result = await DialogHost.Show(new ConfirmViewModel(
"Warning!",
message,
"Delete Report",
"Cancel")
{
ConfirmButtonUsesDangerStyle = true,
ConfirmTitleIcon = "\uf1f8;"
});
if (result != null && (bool)result)
{
SavedReports.Remove(reportInfo);
_settings.AllReportInfo.Remove(reportInfo);
reportInfo.DeleteInternalFolderFromDisk(); // delete internal data if available
await _settings.SaveSettingsAsync(); // update saved items list
}
}
public void ShowAbout()
{
DialogHost.Show(new AboutViewModel());
}
public async Task ShowSettings()
{
var updatedSettings = await DialogHost.Show(new SettingsViewModel(_settings, TopLevelGrabber));
if (updatedSettings != null)
{
_settings = (Settings)updatedSettings;
await _settings.SaveSettingsAsync();
}
}
public async Task<bool> CheckIsSafeToShutdown()
{
return true;
}
public async void UpdateRecentlyUsed(PDFReport report)
{
var didFind = false;
foreach (var existing in _settings.AllReportInfo)
{
if (existing.UUID == report.UUID)
{
didFind = true;
// update info on existing object
existing.LastSaved = report.LastSaved;
existing.Title = report.Title;
existing.BaseFolder = report.BaseFolder;
}
}
if (!didFind)
{
_settings.AllReportInfo.Add(report);
}
// ... this sort and save is slow, technically, but we're not going to have millions of items here, so...
SavedReports = new ObservableCollection<PDFReportInfo>(_settings.AllReportInfo.OrderBy(x => x.Title));
await _settings.SaveSettingsAsync();
}
}
@@ -8,7 +8,7 @@ class WarningDeleteItemViewModel : ChangeNotifier
{ {
ReportFile _file; ReportFile _file;
public WarningDeleteItemViewModel(ReportFile file) public WarningDeleteItemViewModel(ReportFile file): base()
{ {
_file = file; _file = file;
} }
@@ -20,16 +20,21 @@
MaxWidth="350" MaxWidth="350"
Text="{Binding Message}"/> Text="{Binding Message}"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="12" Spacing="16"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="4"> Margin="4">
<Button Command="{Binding Decline}" <Button Command="{Binding Decline}"
Content="{Binding DeclineTitle}" Content="{Binding DeclineTitle}"
HorizontalAlignment="Right"/> HorizontalAlignment="Right"/>
<Button Command="{Binding Confirm}" <Button Command="{Binding Confirm}"
Classes="accent" Classes.accent="{Binding ConfirmButtonIsAccent}"
Content="{Binding ConfirmTitle}" Classes.Danger="{Binding ConfirmButtonIsDanger}"
HorizontalAlignment="Right"/> HorizontalAlignment="Right">
<TextBlock>
<Run Text="{Binding ConfirmTitleIcon}"
FontFamily="{StaticResource FontAwesomeSolid}" /> <Run Text="{Binding ConfirmTitle}"/>
</TextBlock>
</Button>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>
@@ -3,62 +3,36 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MayShow.Views.MainView" x:Class="MayShow.Views.CreatePDFReportView"
xmlns:helpers="clr-namespace:MayShow.Helpers" xmlns:helpers="clr-namespace:MayShow.Helpers"
xmlns:models="clr-namespace:MayShow.Models" xmlns:models="clr-namespace:MayShow.Models"
xmlns:views="clr-namespace:MayShow.Views" xmlns:views="clr-namespace:MayShow.Views"
xmlns:vm="clr-namespace:MayShow.ViewModels" xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:progRing="clr-namespace:AvaloniaProgressRing;assembly=AvaloniaProgressRing" xmlns:progRing="clr-namespace:AvaloniaProgressRing;assembly=AvaloniaProgressRing"
x:DataType="vm:MainViewModel"> x:DataType="vm:CreatePDFReportViewModel">
<UserControl.Resources> <UserControl.Resources>
<helpers:DateFormatConverter x:Key="DateFormatter" /> <helpers:DateFormatConverter x:Key="DateFormatter" />
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="*" <Grid ColumnDefinitions="*"
RowDefinitions="Auto, 2*, Auto, Auto, *"> RowDefinitions="Auto, 2*, Auto, Auto, *">
<Button Command="{Binding ShowSettings}"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="4,4,0,4">
<TextBlock><Run Text="&#xf013;" FontFamily="{StaticResource FontAwesomeSolid}"/> Settings</TextBlock>
</Button>
<Button Command="{Binding ShowAbout}"
Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,4,4,4">
<TextBlock><Run Text="&#xf059;" FontFamily="{StaticResource FontAwesomeSolid}"/> About</TextBlock>
</Button>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="2" Spacing="2"
Margin="0,4,0,0"> Margin="0,4,0,0">
<Label Content="MayShow: Report Builder" <Button Command="{Binding ReturnToMainMenu}"
FontSize="20" Grid.Row="0"
FontWeight="Bold" HorizontalAlignment="Left"
HorizontalAlignment="Center"/> VerticalAlignment="Top"
<Grid ColumnDefinitions="Auto, *" Margin="4,4,0,4">
Margin="4,0,0,0"> <TextBlock><Run Text="&#xf060;" FontFamily="{StaticResource FontAwesomeSolid}"/> Return to Main Menu</TextBlock>
<Button Content="Choose Receipt Folder" </Button>
Command="{Binding ChooseFolder}" <Label Content="Report Title" />
IsEnabled="{Binding !IsCreatingPDF}"
Grid.Column="0" />
<TextBlock Text="{Binding WorkingFolder}"
VerticalAlignment="Center"
TextWrapping="NoWrap"
Margin="4,0,4,0"
TextTrimming="PrefixCharacterEllipsis"
Grid.Column="1"/>
</Grid>
<Label Content="Report Title"
IsVisible="{Binding IsTitleBoxVisible}" />
<TextBox Text="{Binding ReportTitle}" <TextBox Text="{Binding ReportTitle}"
IsVisible="{Binding IsTitleBoxVisible}"
Watermark="Receipts December 2024" Watermark="Receipts December 2024"
Margin="2,0,2,4" Margin="2,0,2,4"
Classes="clearButton" Classes="clearButton"
Name="TitleTextBox"> Name="TitleTextBox">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Command="{Binding $parent[views:MainView].UnfocusTextbox}" Gesture="Enter" /> <KeyBinding Command="{Binding $parent[views:CreatePDFReportView].UnfocusTextbox}" Gesture="Enter" />
</TextBox.KeyBindings> </TextBox.KeyBindings>
</TextBox> </TextBox>
</StackPanel> </StackPanel>
@@ -100,7 +74,7 @@
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Grid ColumnDefinitions="Auto, *"> <Grid ColumnDefinitions="Auto, *">
<Button Command="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).LocateFile}" <Button Command="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).LocateFile}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
IsVisible="{Binding !IsFileFoundOnDisk}" IsVisible="{Binding !IsFileFoundOnDisk}"
Margin="2" Margin="2"
@@ -110,7 +84,7 @@
Grid.Column="0" Grid.Column="0"
FontFamily="{StaticResource FontAwesomeSolid}" FontFamily="{StaticResource FontAwesomeSolid}"
ToolTip.Tip="File not found; click to locate..." ToolTip.Tip="File not found; click to locate..."
IsEnabled="{Binding !$parent[UserControl].((vm:MainViewModel)DataContext).IsCreatingPDF}"/> IsEnabled="{Binding !$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).IsCreatingPDF}"/>
<TextBlock Text="{Binding Title}" <TextBlock Text="{Binding Title}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" TextWrapping="NoWrap"
@@ -128,7 +102,7 @@
ToolTip.Tip="{Binding Title}" ToolTip.Tip="{Binding Title}"
Classes="clearButton"> Classes="clearButton">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Command="{Binding $parent[views:MainView].UnfocusTextbox}" Gesture="Enter" /> <KeyBinding Command="{Binding $parent[views:CreatePDFReportView].UnfocusTextbox}" Gesture="Enter" />
</TextBox.KeyBindings> </TextBox.KeyBindings>
</TextBox> </TextBox>
</DataTemplate> </DataTemplate>
@@ -145,7 +119,7 @@
<Label.Content> <Label.Content>
<MultiBinding Converter="{StaticResource DateFormatter}"> <MultiBinding Converter="{StaticResource DateFormatter}">
<Binding Path="ReceiptDate" /> <Binding Path="ReceiptDate" />
<Binding Path="$parent[UserControl].((vm:MainViewModel)DataContext).DataGridDateFormat" /> <Binding Path="$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormat" />
</MultiBinding> </MultiBinding>
</Label.Content> </Label.Content>
</Label> </Label>
@@ -157,8 +131,8 @@
DisplayDate="{Binding ReceiptDateTime}" DisplayDate="{Binding ReceiptDateTime}"
SelectedDateFormat="Custom" SelectedDateFormat="Custom"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Watermark="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).DataGridDateFormatWatermark}" Watermark="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormatWatermark}"
CustomDateFormatString="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).DataGridDateFormat}"/> CustomDateFormatString="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormat}"/>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@@ -169,7 +143,7 @@
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding FileName}" <TextBlock Text="{Binding FileName}"
VerticalAlignment="Center" VerticalAlignment="Center"
TextTrimming="PrefixCharacterEllipsis" TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding FileName}" ToolTip.Tip="{Binding FileName}"
Margin="8,0,8,0" Margin="8,0,8,0"
HorizontalAlignment="Left"/> HorizontalAlignment="Left"/>
@@ -183,22 +157,22 @@
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="4"> Spacing="4">
<Button Command="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).EditFileProperties}" <Button Command="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).EditFileProperties}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Classes="accent" Classes="accent"
Margin="2" Margin="2"
FontSize="12" FontSize="12"
IsEnabled="{Binding !$parent[UserControl].((vm:MainViewModel)DataContext).IsCreatingPDF}"> IsEnabled="{Binding !$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).IsCreatingPDF}">
<Button.Content> <Button.Content>
<TextBlock><Run Text="&#xf044;" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock> <TextBlock><Run Text="&#xf044;" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock>
</Button.Content> </Button.Content>
</Button> </Button>
<Button Command="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).RemoveFile}" <Button Command="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).RemoveFile}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Classes="Danger" Classes="Danger"
Margin="2" Margin="2"
FontSize="12" FontSize="12"
IsEnabled="{Binding !$parent[UserControl].((vm:MainViewModel)DataContext).IsCreatingPDF}"> IsEnabled="{Binding !$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).IsCreatingPDF}">
<Button.Content> <Button.Content>
<TextBlock><Run Text="&#xf1f8;" FontFamily="{StaticResource FontAwesomeSolid}"/> Remove</TextBlock> <TextBlock><Run Text="&#xf1f8;" FontFamily="{StaticResource FontAwesomeSolid}"/> Remove</TextBlock>
</Button.Content> </Button.Content>
@@ -222,13 +196,13 @@
Spacing="8" Spacing="8"
Margin="4" Margin="4"
Grid.Row="2"> Grid.Row="2">
<Button Command="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).OpenFileLocation}" <Button Command="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).OpenFileLocation}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">
<Button.Content> <Button.Content>
<TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File Location</TextBlock> <TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File Location</TextBlock>
</Button.Content> </Button.Content>
</Button> </Button>
<Button Command="{Binding $parent[UserControl].((vm:MainViewModel)DataContext).OpenFile}" <Button Command="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).OpenFile}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">
<Button.Content> <Button.Content>
<TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock> <TextBlock FontSize="12"><Run Text="&#xf07c;" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock>
@@ -248,7 +222,7 @@
Spacing="4"> Spacing="4">
<Button Command="{Binding AddItem}" <Button Command="{Binding AddItem}"
IsEnabled="{Binding CanAddItem}"> 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 RemoveAllItems}" <Button Command="{Binding RemoveAllItems}"
IsEnabled="{Binding IsCreatePDFButtonEnabled}" IsEnabled="{Binding IsCreatePDFButtonEnabled}"
@@ -260,7 +234,8 @@
<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 SaveInterimReportInfo}" <Button Command="{Binding SaveInterimReportInfo}"
IsEnabled="{Binding HasWorkingFolderAndNotMakingPDF}"> Classes.accent="{Binding IsSaveButtonAccentOn}"
IsEnabled="{Binding !IsCreatingPDF}">
<TextBlock><Run Text="&#xf0c7;" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock> <TextBlock><Run Text="&#xf0c7;" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock>
</Button> </Button>
</StackPanel> </StackPanel>
@@ -7,9 +7,9 @@ using MayShow.ViewModels;
namespace MayShow.Views; namespace MayShow.Views;
public partial class MainView : UserControl public partial class CreatePDFReportView : UserControl
{ {
public MainView() public CreatePDFReportView()
{ {
this.InitializeComponent(); this.InitializeComponent();
LogBlock.PropertyChanged += LogBlock_PropertyChanged; LogBlock.PropertyChanged += LogBlock_PropertyChanged;
@@ -28,7 +28,7 @@ public partial class MainView : UserControl
{ {
var topLevel = TopLevel.GetTopLevel(this); var topLevel = TopLevel.GetTopLevel(this);
topLevel?.FocusManager?.ClearFocus(); topLevel?.FocusManager?.ClearFocus();
if (DataContext is MainViewModel mvm) if (DataContext is CreatePDFReportViewModel mvm)
{ {
mvm?.HasUnsavedWork = true; mvm?.HasUnsavedWork = true;
} }
@@ -36,7 +36,7 @@ public partial class MainView : UserControl
private void FileCellEditEnded(object? sender, DataGridCellEditEndedEventArgs args) private void FileCellEditEnded(object? sender, DataGridCellEditEndedEventArgs args)
{ {
if (args.EditAction == DataGridEditAction.Commit && DataContext is MainViewModel mvm) if (args.EditAction == DataGridEditAction.Commit && DataContext is CreatePDFReportViewModel mvm)
{ {
mvm?.HasUnsavedWork = true; mvm?.HasUnsavedWork = true;
} }
+62
View File
@@ -0,0 +1,62 @@
<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.EditFile"
xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:EditFileViewModel"
MaxWidth="350">
<Grid RowDefinitions="Auto, *, Auto">
<Label Content="Edit File Details"
Grid.Row="0"
HorizontalAlignment="Center"
FontSize="16"
FontWeight="Bold" />
<ScrollViewer AllowAutoHide="False"
Grid.Row="1">
<StackPanel Orientation="Vertical"
Spacing="4"
Margin="12,4,12,0">
<Label Content="Title" />
<TextBox Watermark="Title"
Text="{Binding ClonedFile.Title}"
VerticalAlignment="Stretch"
TextWrapping="Wrap" />
<Label Content="Notes" />
<TextBox Watermark="Notes"
Text="{Binding ClonedFile.Notes}"
VerticalAlignment="Stretch"
AcceptsReturn="True"
ScrollViewer.AllowAutoHide="False"
Height="75" />
<Label Content="Receipt Date" />
<Calendar SelectionMode="SingleDate"
SelectedDate="{Binding ClonedFile.ReceiptDateTime}"
DisplayDate="{Binding ClonedFile.ReceiptDateTime}"
IsTodayHighlighted="False" />
</StackPanel>
</ScrollViewer>
<StackPanel Orientation="Horizontal"
Spacing="12"
Margin="0,8,0,0"
Grid.Row="2"
HorizontalAlignment="Right">
<Button Command="{Binding Cancel}">
<TextBlock>
<Run Text="&#xf0e2;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Cancel</TextBlock>
</Button>
<Button Command="{Binding Save}"
Classes="accent">
<TextBlock>
<Run Text="&#xf0c7;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Save</TextBlock>
</Button>
</StackPanel>
</Grid>
</UserControl>
+19
View File
@@ -0,0 +1,19 @@
<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"
xmlns:vm="using:MayShow.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MayShow.Views.MainView"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:MainViewModel">
<dialogHost:DialogHost CloseOnClickAway="False"
Identifier="DialogHost"
x:Name="WindowDialogHost">
<dialogHost:DialogHost.DialogContent>
<StackPanel/>
</dialogHost:DialogHost.DialogContent>
<!-- put the content over which the dialog is shown here (e.g. your main window grid)-->
<ContentControl Content="{Binding CurrentViewModel}"/>
</dialogHost:DialogHost>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace MayShow.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
}
@@ -7,9 +7,14 @@
d:DesignHeight="450" d:DesignHeight="450"
x:Class="MayShow.Views.SettingsView" x:Class="MayShow.Views.SettingsView"
xmlns:models="clr-namespace:MayShow.Models" xmlns:models="clr-namespace:MayShow.Models"
xmlns:enums="clr-namespace:MayShow.Enums"
xmlns:vm="clr-namespace:MayShow.ViewModels" xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:converters="using:Avalonia.Controls.Converters"
x:DataType="vm:SettingsViewModel" x:DataType="vm:SettingsViewModel"
MaxWidth="450"> MaxWidth="450">
<UserControl.Resources>
<converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
</UserControl.Resources>
<ScrollViewer AllowAutoHide="False"> <ScrollViewer AllowAutoHide="False">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Spacing="8" Spacing="8"
@@ -32,10 +37,32 @@
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="0,0,0,4" /> Margin="0,0,0,4" />
<CheckBox IsChecked="{Binding !UseDocnetPDFImageRendering}">Use legacy PDF handling (does not work with macOS annotations)</CheckBox> <CheckBox IsChecked="{Binding !UseDocnetPDFImageRendering}">Use legacy PDF handling (does not work with macOS annotations)</CheckBox>
<CheckBox IsChecked="{Binding SaveOutputPdfInWorkingDir}">Always save report PDF in working directory</CheckBox> <StackPanel Spacing="4">
<Label HorizontalAlignment="Left">
Save created PDF Report in the following location:
</Label>
<RadioButton IsChecked="{Binding PDFOutputSaveLocation,
Converter={StaticResource EnumToBoolConverter},
ConverterParameter={x:Static enums:PDFSaveLocation.BaseFolder}}">
<TextBlock TextWrapping="Wrap"
Text="MayShow application directory (uses report title as file name)"/>
</RadioButton>
<RadioButton IsChecked="{Binding PDFOutputSaveLocation,
Converter={StaticResource EnumToBoolConverter},
ConverterParameter={x:Static enums:PDFSaveLocation.AlwaysAsk}}">
<TextBlock TextWrapping="Wrap"
Text="Ask me where and with what file name I want to save the PDF file every time"/>
</RadioButton>
<RadioButton IsChecked="{Binding PDFOutputSaveLocation,
Converter={StaticResource EnumToBoolConverter},
ConverterParameter={x:Static enums:PDFSaveLocation.OtherChosenDir}}">
<TextBlock TextWrapping="Wrap"
Text="Always save PDF file in the same folder of my choice (uses report title as file name)"/>
</RadioButton>
</StackPanel>
<Grid ColumnDefinitions="Auto, *" <Grid ColumnDefinitions="Auto, *"
IsVisible="{Binding !SaveOutputPdfInWorkingDir}"> IsVisible="{Binding SaveOutputPdfInChosenDir}">
<Button Content="Choose report output folder..." <Button Content="Choose PDF report output folder..."
Grid.Column="0" Grid.Column="0"
Command="{Binding ChooseOutputFolder}" Command="{Binding ChooseOutputFolder}"
VerticalAlignment="Top"/> VerticalAlignment="Top"/>
@@ -72,7 +99,6 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<CheckBox IsChecked="{Binding SaveReportJsonDataInInternalDir}">Save report data (names, notes, etc.) in MayShow settings directory (saves in working directory by default)</CheckBox>
<Button Command="{Binding OpenSettingsDir}"> <Button Command="{Binding OpenSettingsDir}">
<TextBlock> <TextBlock>
<Run Text="&#xf07c;" <Run Text="&#xf07c;"
@@ -18,7 +18,7 @@
FontWeight="Bold" FontWeight="Bold"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="14" FontSize="14"
Text="Do you want to save your data before the program is closed?"/> Text="Do you want to save your data before closing?"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="8"> Spacing="8">
<Button Command="{Binding SaveAndShutdown}" <Button Command="{Binding SaveAndShutdown}"
@@ -31,7 +31,7 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="0,4,0,4"/> Margin="0,4,0,4"/>
<Button Command="{Binding CancelShutdown}" <Button Command="{Binding CancelShutdown}"
Content="Cancel Program Shutdown" Content="Cancel"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="0,4,0,4"/> Margin="0,4,0,4"/>
</StackPanel> </StackPanel>
@@ -0,0 +1,126 @@
<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.StartNewChooseReport"
xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:StartNewChooseReportViewModel">
<Grid ColumnDefinitions="100, *, 100"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, *">
<Button Command="{Binding ShowSettings}"
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="4,4,0,4">
<TextBlock><Run Text="&#xf013;" FontFamily="{StaticResource FontAwesomeSolid}"/> Settings</TextBlock>
</Button>
<Button Command="{Binding ShowAbout}"
Grid.Row="0"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,4,4,4">
<TextBlock><Run Text="&#xf059;" FontFamily="{StaticResource FontAwesomeSolid}"/> About</TextBlock>
</Button>
<TextBlock HorizontalAlignment="Center"
FontSize="36"
FontWeight="Bold"
Margin="0,16,0,0"
Grid.Column="1"
Grid.Row="0">
MayShow <Run Text="{Binding Version}"/>
</TextBlock>
<Image Source="avares://MayShow/Assets/MayShowIcon.png"
Width="125"
Margin="0,12,0,0"
HorizontalAlignment="Center"
Grid.Column="1"
Grid.Row="1" />
<Label Content="Start New Report"
HorizontalAlignment="Left"
FontSize="16"
FontWeight="Bold"
Margin="0,24,0,0"
Grid.Column="1"
Grid.Row="2"/>
<Grid HorizontalAlignment="Stretch"
ColumnDefinitions="*, Auto"
Margin="0,8,0,0"
Grid.Column="1"
Grid.Row="3">
<TextBox Watermark="Feb 2024 Report"
Classes="clearButton"
Text="{Binding CreatingReportTitle}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
TextWrapping="Wrap"
Margin="0,0,16,0"
Grid.Column="0" />
<Button Command="{Binding StartReport}"
Classes="accent"
Grid.Column="1">
<TextBlock>
<Run Text="&#xe494;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Create Blank Report</TextBlock>
</Button>
</Grid>
<Label Content="Load Previously Saved Report"
FontSize="16"
FontWeight="Bold"
HorizontalAlignment="Left"
Margin="0,16,0,0"
Grid.Column="1"
Grid.Row="4"/>
<ScrollViewer Margin="0,8,0,32"
Grid.Column="1"
Grid.Row="5"
VerticalScrollBarVisibility="Visible"
AllowAutoHide="False"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding SavedReports}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PDFReportInfo">
<StackPanel Orientation="Vertical"
Margin="12,4,4,8"
Spacing="4">
<TextBlock Text="{Binding Title}" />
<!-- <TextBlock Text="{Binding BaseFolder}"
Foreground="Gray"
TextWrapping="Wrap" /> -->
<TextBlock Foreground="Gray"
TextWrapping="Wrap">
Last saved on: <Run Text="{Binding LastSaved, StringFormat='{}{0:yyyy-MM-dd}'}"/>
</TextBlock>
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Command="{Binding $parent[UserControl].((vm:StartNewChooseReportViewModel)DataContext).LoadExistingReport}"
CommandParameter="{Binding}"
Classes="accent"
Grid.Column="1">
<TextBlock>
<Run Text="&#xf56f;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Load Report</TextBlock>
</Button>
<Button Command="{Binding $parent[UserControl].((vm:StartNewChooseReportViewModel)DataContext).DeleteExistingReport}"
CommandParameter="{Binding}"
Classes="Danger"
Grid.Column="1">
<TextBlock>
<Run Text="&#xf1f8;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Delete Report</TextBlock>
</Button>
</StackPanel>
<Rectangle Fill="Gray" Height="2" HorizontalAlignment="Stretch" Margin="0,8,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
@@ -0,0 +1,14 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MayShow.Views;
public partial class StartNewChooseReport : UserControl
{
public StartNewChooseReport()
{
this.InitializeComponent();
}
}
+23
View File
@@ -0,0 +1,23 @@
using Foundation;
using UIKit;
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Media;
namespace MayShow.iOS;
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
public partial class AppDelegate : AvaloniaAppDelegate<App>
#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.WithInterFont();
}
}
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>MayShow</string>
<key>CFBundleIdentifier</key>
<string>companyName.MayShow</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>13.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+14
View File
@@ -0,0 +1,14 @@
using UIKit;
namespace MayShow.iOS;
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-ios</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<UseInterpreter>True</UseInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.iOS" Version="$(AvaloniaVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MayShow.Shared\MayShow.Shared.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21" />
<fontDescription key="fontDescription" type="system" pointSize="17" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="MayShow" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43" />
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
</constraints>
<nil key="simulatedStatusBarMetrics" />
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
<point key="canvasLocation" x="548" y="455" />
</view>
</objects>
</document>
+5
View File
@@ -0,0 +1,5 @@
<Solution>
<Project Path="MayShow.Desktop/MayShow.Desktop.csproj" />
<Project Path="MayShow.iOS/MayShow.iOS.csproj" />
<Project Path="MayShow.Shared/MayShow.Shared.csproj" />
</Solution>
-58
View File
@@ -1,58 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using MayShow.Helpers;
namespace MayShow.Models;
class PDFReport : ChangeNotifier
{
private string _baseFolder;
private string _title;
private List<ReportFile> _files;
private DateTime _lastSaved;
private DateTime? _lastGenerated;
public PDFReport()
{
_baseFolder = "";
_title = "";
_files = [];
_lastSaved = DateTime.Now;
_lastGenerated = null;
}
public string BaseFolder
{
get => _baseFolder;
set { _baseFolder = value; NotifyPropertyChanged(); }
}
public string Title
{
get => _title;
set { _title = value; NotifyPropertyChanged(); }
}
public List<ReportFile> Files
{
get => _files;
set { _files = value; NotifyPropertyChanged(); }
}
public DateTime LastSaved
{
get => _lastSaved;
set { _lastSaved = value; NotifyPropertyChanged(); }
}
public DateTime? LastGenerated
{
get => _lastGenerated;
set { _lastGenerated = value; NotifyPropertyChanged(); }
}
}
-176
View File
@@ -1,176 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using MayShow.Helpers;
namespace MayShow.Models;
class Settings : ChangeNotifier
{
private string _lastUsedPath;
private bool _useDocnetPDFImageRendering;
private bool _saveOutputPdfInWorkingDir;
private string _outputPdfDir;
private decimal _imageResizeThreshold;
private bool _saveReportJsonDataInInternalDir;
private Dictionary<string, string> _workingFolderToInternalFolderName;
public string _dataGridDateFormat;
public string _reportDateFormat;
public int _settingsVersion;
public Settings()
{
_lastUsedPath = "";
_useDocnetPDFImageRendering = true;
_saveOutputPdfInWorkingDir = true;
_outputPdfDir = "";
_imageResizeThreshold = 1.5m;
_saveReportJsonDataInInternalDir = false;
_workingFolderToInternalFolderName = [];
_settingsVersion = 1;
_dataGridDateFormat = "dd/MM/yyyy";
_reportDateFormat = "yyyy-MM-dd";
}
public Settings(Settings other)
{
_lastUsedPath = other.LastUsedPath;
_useDocnetPDFImageRendering = other.UseDocnetPDFImageRendering;
_saveOutputPdfInWorkingDir = other.SaveOutputPdfInWorkingDir;
_outputPdfDir = other.OutputPdfDir;
_imageResizeThreshold = other.ImageResizeThreshold;
_saveReportJsonDataInInternalDir = other.SaveReportJsonDataInInternalDir;
_workingFolderToInternalFolderName = other.WorkingFolderToInternalFolderName;
_settingsVersion = other.SettingsVersion;
_dataGridDateFormat = "yyyy-MM-dd";
_reportDateFormat = "yyyy-MM-dd";
}
[JsonInclude]
public string LastUsedPath
{
get => _lastUsedPath;
set { _lastUsedPath = value; NotifyPropertyChanged(); }
}
[JsonInclude]
[JsonPropertyName("UseDocnetPFDImageRendering")] // ...this typo now has to live because people have this saved on disk...
public bool UseDocnetPDFImageRendering
{
get => _useDocnetPDFImageRendering;
set { _useDocnetPDFImageRendering = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public bool SaveOutputPdfInWorkingDir
{
get => _saveOutputPdfInWorkingDir;
set { _saveOutputPdfInWorkingDir = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string OutputPdfDir
{
get => _outputPdfDir;
set { _outputPdfDir = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public decimal ImageResizeThreshold
{
get => _imageResizeThreshold;
set { _imageResizeThreshold = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public bool SaveReportJsonDataInInternalDir
{
get => _saveReportJsonDataInInternalDir;
set { _saveReportJsonDataInInternalDir = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public Dictionary<string, string> WorkingFolderToInternalFolderName
{
get => _workingFolderToInternalFolderName;
set { _workingFolderToInternalFolderName = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public int SettingsVersion
{
get => _settingsVersion;
set { _settingsVersion = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string DataGridDateFormat
{
get => _dataGridDateFormat;
set { _dataGridDateFormat = value; NotifyPropertyChanged(); }
}
[JsonInclude]
public string ReportDateFormat
{
get => _reportDateFormat;
set { _reportDateFormat = value; NotifyPropertyChanged(); }
}
public static string SettingsFileName = "settings.json";
public static string GetSettingsPath()
{
var path = Utilities.GetInternalDataPath();
return Path.Combine(path, SettingsFileName);
}
public string SaveSettingsNotAsync()
{
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
using MemoryStream memoryStream = new MemoryStream();
JsonSerializer.Serialize(memoryStream, this, jsonContext.Settings);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var json = reader.ReadToEnd();
File.WriteAllText(GetSettingsPath(), json);
return json;
}
public async Task<string> SaveSettingsAsync()
{
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
using MemoryStream memoryStream = new MemoryStream();
await JsonSerializer.SerializeAsync(memoryStream, this, jsonContext.Settings);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream);
var json = await reader.ReadToEndAsync();
await File.WriteAllTextAsync(GetSettingsPath(), json);
return json;
}
public static Settings LoadSettings()
{
var path = GetSettingsPath();
if (!File.Exists(path))
{
return new Settings();
}
var json = File.ReadAllText(GetSettingsPath());
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
return JsonSerializer.Deserialize<Settings>(json, jsonContext.Settings) ?? new Settings();
}
public static async Task<Settings> LoadSettingsAsync()
{
using FileStream fileStream = File.OpenRead(GetSettingsPath());
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
var output = await JsonSerializer.DeserializeAsync<Settings>(fileStream, jsonContext.Settings) ?? new Settings();
return output;
}
}
-33
View File
@@ -1,33 +0,0 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Avalonia.Themes.Fluent;
using DialogHostAvalonia;
using ImageMagick;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using PdfSharp.Fonts;
using PdfSharp.Pdf.IO;
using PdfSharp.Snippets.Font;
using MayShow.Interfaces;
using MayShow.Models;
namespace MayShow.ViewModels;
class AboutViewModel
{
public AboutViewModel()
{
}
public void Close()
{
DialogHost.Close("DialogHost", null);
}
}
File diff suppressed because it is too large Load Diff
-57
View File
@@ -1,57 +0,0 @@
<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.EditFile"
xmlns:models="clr-namespace:MayShow.Models"
xmlns:vm="clr-namespace:MayShow.ViewModels"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
x:DataType="vm:EditFileViewModel"
MaxWidth="350">
<ScrollViewer AllowAutoHide="False">
<StackPanel Orientation="Vertical"
Spacing="4"
Margin="12,4,12,0">
<Label Content="Edit File Details"
HorizontalAlignment="Center"
FontSize="16"
FontWeight="Bold" />
<Label Content="Title" />
<TextBox Watermark="Title"
Text="{Binding ClonedFile.Title}"
VerticalAlignment="Stretch"
TextWrapping="Wrap" />
<Label Content="Notes" />
<TextBox Watermark="Notes"
Text="{Binding ClonedFile.Notes}"
VerticalAlignment="Stretch"
AcceptsReturn="True"
ScrollViewer.AllowAutoHide="False"
Height="75" />
<Label Content="Receipt Date" />
<Calendar SelectionMode="SingleDate"
SelectedDate="{Binding ClonedFile.ReceiptDateTime}"
DisplayDate="{Binding ClonedFile.ReceiptDateTime}"
IsTodayHighlighted="False" />
<StackPanel Orientation="Horizontal"
Spacing="12"
Margin="0,4,0,0"
HorizontalAlignment="Right">
<Button Command="{Binding Cancel}">
<TextBlock>
<Run Text="&#xf0e2;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Cancel</TextBlock>
</Button>
<Button Command="{Binding Save}"
Classes="accent">
<TextBlock>
<Run Text="&#xf0c7;"
FontFamily="{StaticResource FontAwesomeSolid}" /> Save</TextBlock>
</Button>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>