Compare commits
120 Commits
1.3.0
...
feature/ios
| Author | SHA1 | Date | |
|---|---|---|---|
| 60361b4913 | |||
| 42f7de5d5c | |||
| 236eccf3d1 | |||
| e2408ce581 | |||
| b0c897bef9 | |||
| 82e3289e21 | |||
| 004647008f | |||
| 981114a222 | |||
| e813504453 | |||
| 71ca0c6459 | |||
| d5b174fa22 | |||
| a58560efb3 | |||
| cd9fce14fc | |||
| 135647c591 | |||
| ed26989678 | |||
| 287ef24113 | |||
| 8a502ce820 | |||
| d5c188ed2e | |||
| eae459e047 | |||
| c5e3276f34 | |||
| afed4c8438 | |||
| c3ec91027f | |||
| 411d70aa18 | |||
| 14fc5b29ee | |||
| d99fefe74d | |||
| dd09459226 | |||
| c3d6c0c846 | |||
| 69a885ff57 | |||
| 81a11a9b84 | |||
| 4dfcd991a6 | |||
| f2c4d1ed53 | |||
| e47d4bcbc1 | |||
| 7be1e16489 | |||
| 73e959a677 | |||
| 03d2cf30d0 | |||
| 0d07bc468c | |||
| 80bc33b85e | |||
| 3482ed7336 | |||
| 5b0508721f | |||
| 69292b6410 | |||
| 4d722c0220 | |||
| 6fa58d3a62 | |||
| 15038f5feb | |||
| 3b776e2534 | |||
| ec99595303 | |||
| 5fd61f1f84 | |||
| 23e8b2533a | |||
| 4d89e49c96 | |||
| 58c59dfc0a | |||
| 867f57dcff | |||
| 72abb6cbb3 | |||
| 62307e09a1 | |||
| f39b643b00 | |||
| fe6b8c684f | |||
| ddba84ac0b | |||
| b0661669b4 | |||
| 249cc8724d | |||
| 3fddda26c9 | |||
| e7c8328630 | |||
| 9391e868a2 | |||
| a0ef69e8ea | |||
| 2092f15580 | |||
| a2452e2585 | |||
| 3e0852ead9 | |||
| faf9fc488e | |||
| b22dbdff03 | |||
| 1785a91d6d | |||
| 22d2a0d5e0 | |||
| 4c85b4b606 | |||
| 22d896543c | |||
| 534af58f44 | |||
| a9674a3f45 | |||
| 8be518e81c | |||
| a072bff548 | |||
| fa82407915 | |||
| fcee3b09e6 | |||
| b8b1effcf2 | |||
| e71de9016e | |||
| d90cd1354f | |||
| f4dd498d22 | |||
| cd71df8a8e | |||
| de621fe9dc | |||
| a1858443e8 | |||
| f34a7092e4 | |||
| aed85c7555 | |||
| 25f739667d | |||
| 1fae3f5341 | |||
| 5dda88521d | |||
| 742ecb148a | |||
| 4ad4a0852f | |||
| c53b41658a | |||
| 80eda8bc5b | |||
| 7d4c440176 | |||
| b0cc2c4f6b | |||
| c44093c5ee | |||
| 9e7ce7ef57 | |||
| 385f794b03 | |||
| 182055d19e | |||
| c258584504 | |||
| 71f4fda2d4 | |||
| 478f67aa82 | |||
| 1efd9993bf | |||
| 9dd843ea69 | |||
| 1132d475b1 | |||
| b00ad7669c | |||
| 27aff81016 | |||
| 8da9574e2b | |||
| 32228e143f | |||
| 94c8945140 | |||
| 94cf073012 | |||
| abc97eea2b | |||
| 3e91edb457 | |||
| 03da58277e | |||
| 193f57df02 | |||
| 97edee3ef5 | |||
| e822963d60 | |||
| 03aa1aadc9 | |||
| b359c8b15f | |||
| 0f37f189ac | |||
| 445c7bd353 |
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
; Non-commercial use only
|
; Non-commercial use only
|
||||||
|
|
||||||
#define MyAppName "MayShow"
|
#define MyAppName "MayShow"
|
||||||
#define MyAppVersion "1.3.0"
|
#define MyAppVersion "1.4.3"
|
||||||
#define MyAppPublisher "Quickity Quack Productions"
|
#define MyAppPublisher "Quickity Quack Productions"
|
||||||
#define MyAppExeName "MayShow.exe"
|
#define MyAppExeName "MayShow.exe"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
VERSION="1.3.0"
|
VERSION="1.4.3"
|
||||||
SRC_DIR="src" # user ran script from main folder
|
SRC_DIR="src" # user ran script from main folder
|
||||||
if [ ! -d "$SRC_DIR" ]; then
|
if [ ! -d "$SRC_DIR" ]; then
|
||||||
SRC_DIR= "../src" # try
|
SRC_DIR= "../src" # try
|
||||||
@@ -11,12 +11,15 @@ if [ ! -d "$SRC_DIR" ]; then
|
|||||||
fi
|
fi
|
||||||
cd "$SRC_DIR"
|
cd "$SRC_DIR"
|
||||||
echo "Building release for linux-x64..."
|
echo "Building release for linux-x64..."
|
||||||
|
dotnet clean -c Release
|
||||||
dotnet publish -c Release -r linux-x64 -p:StripSymbols=False -p:PublishAot=False
|
dotnet publish -c Release -r linux-x64 -p:StripSymbols=False -p:PublishAot=False
|
||||||
echo "Zipping up linux-x64..."
|
echo "Zipping up linux-x64..."
|
||||||
cd bin/Release/net10.0/linux-x64/publish
|
cd bin/Release/net10.0/linux-x64/publish
|
||||||
zip -r "../../../../MayShow $VERSION linux-arm64.zip" .
|
zip -r "../../../../MayShow $VERSION linux-x64.zip" .
|
||||||
cd ../../../../../
|
cd ../../../../../
|
||||||
|
# -----
|
||||||
echo "Building release for linux-arm64..."
|
echo "Building release for linux-arm64..."
|
||||||
|
dotnet clean -c Release
|
||||||
dotnet publish -c Release -r linux-arm64 -p:StripSymbols=False -p:PublishAot=False
|
dotnet publish -c Release -r linux-arm64 -p:StripSymbols=False -p:PublishAot=False
|
||||||
cd bin/Release/net10.0/linux-arm64/publish
|
cd bin/Release/net10.0/linux-arm64/publish
|
||||||
zip -r "../../../../MayShow $VERSION linux-arm64.zip" .
|
zip -r "../../../../MayShow $VERSION linux-arm64.zip" .
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AvaloniaVersion>11.3.12</AvaloniaVersion>
|
<AvaloniaVersion>11.3.13</AvaloniaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MayShow.Helpers
|
|
||||||
{
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?view=netframework-4.7.2
|
|
||||||
class ChangeNotifier : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace MayShows.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
|
|
||||||
var rgx = new Regex(@"\d{4}-\d{2}-\d{2}");
|
|
||||||
var mat = rgx.Match(str);
|
|
||||||
if (mat.Success)
|
|
||||||
{
|
|
||||||
var dtStr = mat.ToString();
|
|
||||||
string[] formats = ["yyyy-MM-dd"];
|
|
||||||
DateTime parsedDateTime;
|
|
||||||
var didWork = DateTime.TryParseExact(dtStr, formats, CultureInfo.InvariantCulture,
|
|
||||||
DateTimeStyles.None, out parsedDateTime);
|
|
||||||
return didWork ? DateOnly.FromDateTime(parsedDateTime) : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- This manifest is used on Windows only.
|
<!-- This manifest is used on Windows only.
|
||||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
<assemblyIdentity version="1.3.0.0" name="MayShow.Desktop"/>
|
<assemblyIdentity version="1.4.3.0" name="MayShow.Desktop"/>
|
||||||
|
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
<application>
|
<application>
|
||||||
@@ -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>
|
||||||
@@ -131,7 +134,7 @@
|
|||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
<NativeMenu.Menu>
|
<NativeMenu.Menu>
|
||||||
<NativeMenu>
|
<NativeMenu>
|
||||||
<NativeMenuItem Header="About ReceiptBuilder" Click="AboutOnClick" />
|
<NativeMenuItem Header="About MayShow" Click="AboutOnClick" />
|
||||||
</NativeMenu>
|
</NativeMenu>
|
||||||
</NativeMenu.Menu>
|
</NativeMenu.Menu>
|
||||||
</Application>
|
</Application>
|
||||||
@@ -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());
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
--Avalonia--
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) AvaloniaUI OÜ All Rights Reserved
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--PDFsharp-MigraDoc--
|
||||||
|
|
||||||
|
Copyright (c) 2001-2026 empira Software GmbH, Troisdorf (Cologne Area), Germany
|
||||||
|
|
||||||
|
http://docs.pdfsharp.net
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--ImageMagick--
|
||||||
|
|
||||||
|
Terms and Conditions for Use, Reproduction, and Distribution
|
||||||
|
|
||||||
|
The legally binding and authoritative terms and conditions for use, reproduction, and distribution of ImageMagick follow:
|
||||||
|
|
||||||
|
Copyright © 1999 ImageMagick Studio LLC, a non-profit organization dedicated to making software imaging solutions freely available.
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
License shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
Legal Entity shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
You (or Your) shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
Source form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
Object form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
Work shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
Derivative Works shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
Contribution shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution.
|
||||||
|
|
||||||
|
Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
--Magick.NET--
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
--Deadpikle.AvaloniaProgressRing--
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Deadpikle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--DialogHost.Avalonia--
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 SKProCH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
--Xaml.Behaviors--
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) Wiesław Šoltés
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
--Docnet.Core--
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Modestas Petravicius
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
--SixLabors.ImageSharp--
|
||||||
|
|
||||||
|
(Note: MayShow is open source and thus is licensed to use SixLabors.ImageSharp under the Apache License, Version 2.0)
|
||||||
|
|
||||||
|
Six Labors Split License
|
||||||
|
Version 1.0, June 2022
|
||||||
|
Copyright (c) Six Labors
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including but not limited to software source
|
||||||
|
code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including
|
||||||
|
but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" (or "Works") shall mean any Six Labors software made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work.
|
||||||
|
|
||||||
|
"Direct Package Dependency" shall mean any Work in Source or Object form that is installed directly by You.
|
||||||
|
|
||||||
|
"Transitive Package Dependency" shall mean any Work in Object form that is installed indirectly by a third party
|
||||||
|
dependency unrelated to Six Labors.
|
||||||
|
|
||||||
|
2. License
|
||||||
|
|
||||||
|
Works in Source or Object form are split licensed and may be licensed under the Apache License, Version 2.0 or a
|
||||||
|
Six Labors Commercial Use License.
|
||||||
|
|
||||||
|
Licenses are granted based upon You meeting the qualified criteria as stated. Once granted,
|
||||||
|
You must reference the granted license only in all documentation.
|
||||||
|
|
||||||
|
Works in Source or Object form are licensed to You under the Apache License, Version 2.0 if.
|
||||||
|
|
||||||
|
- You are consuming the Work in for use in software licensed under an Open Source or Source Available license.
|
||||||
|
- You are consuming the Work as a Transitive Package Dependency.
|
||||||
|
- You are consuming the Work as a Direct Package Dependency in the capacity of a For-profit company/individual with
|
||||||
|
less than 1M USD annual gross revenue.
|
||||||
|
- You are consuming the Work as a Direct Package Dependency in the capacity of a Non-profit organization
|
||||||
|
or Registered Charity.
|
||||||
|
|
||||||
|
For all other scenarios, Works in Source or Object form are licensed to You under the Six Labors Commercial License
|
||||||
|
which may be purchased by visiting https://sixlabors.com/pricing/.
|
||||||
|
|
||||||
|
--FontAwesome--
|
||||||
|
|
||||||
|
Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Font Awesome Free License
|
||||||
|
|
||||||
|
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||||
|
commercial projects, open source projects, or really almost whatever you want.
|
||||||
|
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||||
|
|
||||||
|
The Font Awesome Free download is licensed under a Creative Commons
|
||||||
|
Attribution 4.0 International License and applies to all icons packaged
|
||||||
|
as SVG and JS file types.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Fonts: SIL OFL 1.1 License
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||||
|
packaged as web and desktop font files.
|
||||||
|
|
||||||
|
Copyright (c) 2026 Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
with Reserved Font Name: "Font Awesome".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE
|
||||||
|
Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting — in part or in whole — any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||||
|
non-icon files.
|
||||||
|
|
||||||
|
Copyright 2026 Fonticons, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without limitation the rights to use, copy,
|
||||||
|
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Attribution
|
||||||
|
|
||||||
|
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||||
|
Awesome Free files already contain embedded comments with sufficient
|
||||||
|
attribution, so you shouldn't need to do anything additional when using these
|
||||||
|
files normally.
|
||||||
|
|
||||||
|
We've kept attribution comments terse, so we ask that you do not actively work
|
||||||
|
to remove them from files, especially code. They're a great way for folks to
|
||||||
|
learn about Font Awesome.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Brand Icons
|
||||||
|
|
||||||
|
All brand icons are trademarks of their respective owners. The use of these
|
||||||
|
trademarks does not indicate endorsement of the trademark holder by Font
|
||||||
|
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||||
|
to represent the company, product, or service to which they refer.**
|
||||||
|
|
||||||
|
--Noto Sans Font--
|
||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
--Noto Sans JP Font--
|
||||||
|
|
||||||
|
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MayShow.Helpers;
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?view=netframework-4.7.2
|
||||||
|
class ChangeNotifier : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,37 @@
|
|||||||
|
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
|
using MayShow.Models;
|
||||||
|
|
||||||
namespace MayShow.Helpers;
|
namespace MayShow.Helpers;
|
||||||
|
|
||||||
class Constants
|
class Constants
|
||||||
{
|
{
|
||||||
public static string AppVersion = "1.3.0";
|
public static string AppVersion = "1.4.3";
|
||||||
|
|
||||||
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 List<DateDisplayFormat> GetDateDisplayFormats()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new DateDisplayFormat("Month/Day/Year", "4/5/2026", "M/d/yyyy"),
|
||||||
|
new DateDisplayFormat("Year-Month-Day", "2026-04-05", "yyyy-MM-dd"),
|
||||||
|
new DateDisplayFormat("Month Day, Year", "April 5, 2026", "MMMM d, yyyy"),
|
||||||
|
new DateDisplayFormat("DOW, Month Day, Year", "Sunday, April 5, 2026", "dddd, MMMM d, yyyy"),
|
||||||
|
new DateDisplayFormat("Abbreviated-Month Day, Year", "Apr 5, 2026", "MMM d, yyyy"),
|
||||||
|
new DateDisplayFormat("DOW, Abbreviated-Month Day, Year", "Sunday, Apr 5, 2026", "dddd, MMM d, yyyy"),
|
||||||
|
new DateDisplayFormat("Day Month, Year", "5 April 2026", "d MMMM yyyy"),
|
||||||
|
new DateDisplayFormat("Day Abbreviated-Month, Year", "5 Apr 2026", "d MMM yyyy"),
|
||||||
|
new DateDisplayFormat("Day Month, Year", "05 April 2026", "dd MMMM yyyy"),
|
||||||
|
new DateDisplayFormat("Day Abbreviated-Month, Year", "05 Apr 2026", "dd MMM yyyy"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public static string[] GetQuotes()
|
public static string[] GetQuotes()
|
||||||
{
|
{
|
||||||
// sources:
|
// sources:
|
||||||
+1
-1
@@ -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,35 @@
|
|||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace MayShow.Helpers;
|
||||||
|
|
||||||
|
public class DateFormatConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(
|
||||||
|
IList<object?> values,
|
||||||
|
Type targetType,
|
||||||
|
object? parameter,
|
||||||
|
CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values.Count >= 2 && values[0] is DateOnly date && values[1] is string format)
|
||||||
|
{
|
||||||
|
return date.ToString(format);
|
||||||
|
}
|
||||||
|
if (values.Count >= 2 && values[0] is string dateFormat && values[1] is DateOnly dateOnly)
|
||||||
|
{
|
||||||
|
return dateOnly.ToString(dateFormat);
|
||||||
|
}
|
||||||
|
if (values.Count >= 2 && values[0] is DateTime dateTime && values[1] is string format3)
|
||||||
|
{
|
||||||
|
return dateTime.ToString(format3);
|
||||||
|
}
|
||||||
|
if (values.Count >= 2 && values[0] is string format4 && values[1] is DateTime dateTime2)
|
||||||
|
{
|
||||||
|
return dateTime2.ToString(format4);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace MayShows.Helpers;
|
namespace MayShow.Helpers;
|
||||||
|
|
||||||
public static class ThreadSafeRandom
|
public static class ThreadSafeRandom
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,18 +1,12 @@
|
|||||||
|
|
||||||
<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.3.0</AssemblyVersion> <!-- Also update Constants version -->
|
<AssemblyVersion>1.4.3</AssemblyVersion> <!-- Also update Constants version -->
|
||||||
<ApplicationIcon>MayShow-icon.ico</ApplicationIcon>
|
<ApplicationIcon>MayShow-icon.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -30,15 +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 Include="Assets\Fonts\FontAwesome\*.*">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<PublishFolderType>Resource</PublishFolderType>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Assets\LICENSES.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<PublishFolderType>Resource</PublishFolderType>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -51,8 +57,11 @@
|
|||||||
<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.10.3" />
|
<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 -->
|
||||||
|
<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" />
|
||||||
<PackageReference Include="DialogHost.Avalonia" Version="0.10.4" />
|
<PackageReference Include="DialogHost.Avalonia" Version="0.10.4" />
|
||||||
<PackageReference Include="Xaml.Behaviors.Interactions.DragAndDrop.DataGrid" Version="11.3.9.5" />
|
<PackageReference Include="Xaml.Behaviors.Interactions.DragAndDrop.DataGrid" Version="11.3.9.5" />
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MayShow.Helpers;
|
||||||
|
|
||||||
|
namespace MayShow.Models;
|
||||||
|
|
||||||
|
class DateDisplayFormat : ChangeNotifier
|
||||||
|
{
|
||||||
|
private string _title;
|
||||||
|
private string _example;
|
||||||
|
private string _value;
|
||||||
|
|
||||||
|
public DateDisplayFormat(string title, string example, string value)
|
||||||
|
{
|
||||||
|
_title = title;
|
||||||
|
_example = example;
|
||||||
|
_value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
set { _title = value; NotifyPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Example
|
||||||
|
{
|
||||||
|
get => _example;
|
||||||
|
set { _example = value; NotifyPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set { _value = value; NotifyPropertyChanged(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -23,7 +23,7 @@ class ReportFile : ChangeNotifier
|
|||||||
public ReportFile(ReportFile other)
|
public ReportFile(ReportFile other)
|
||||||
{
|
{
|
||||||
Title = _title = other.Title;
|
Title = _title = other.Title;
|
||||||
ReceiptDateTime = _receiptDateTime = other.ReceiptDateTime;
|
ReceiptDateTime = _receiptDateTime = other.ReceiptDateTime ?? DateTime.Now;
|
||||||
Notes = _notes = other.Notes;
|
Notes = _notes = other.Notes;
|
||||||
FilePath = _filePath = other.FilePath;
|
FilePath = _filePath = other.FilePath;
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,12 @@ class ReportFile : ChangeNotifier
|
|||||||
set { _title = value; NotifyPropertyChanged(); }
|
set { _title = value; NotifyPropertyChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime ReceiptDateTime
|
public DateTime? ReceiptDateTime
|
||||||
{
|
{
|
||||||
get => _receiptDateTime;
|
get => _receiptDateTime;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_receiptDateTime = value;
|
_receiptDateTime = value ?? DateTime.Now;
|
||||||
NotifyPropertyChanged();
|
NotifyPropertyChanged();
|
||||||
NotifyPropertyChanged(nameof(ReceiptDate));
|
NotifyPropertyChanged(nameof(ReceiptDate));
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using MayShow.Helpers;
|
||||||
|
using MayShow.Interfaces;
|
||||||
|
|
||||||
|
namespace MayShow.ViewModels;
|
||||||
|
|
||||||
|
class BaseViewModel : ChangeNotifier
|
||||||
|
{
|
||||||
|
IChangeViewModel _viewModelChanger;
|
||||||
|
ITopLevelGrabber? _topLevelGrabber;
|
||||||
|
|
||||||
|
public BaseViewModel(IChangeViewModel viewModelChanger): base()
|
||||||
|
{
|
||||||
|
_viewModelChanger = viewModelChanger;
|
||||||
|
_topLevelGrabber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITopLevelGrabber? TopLevelGrabber
|
||||||
|
{
|
||||||
|
get => _topLevelGrabber;
|
||||||
|
set { _topLevelGrabber = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChangeViewModel ViewModelChanger
|
||||||
|
{
|
||||||
|
get { return _viewModelChanger; }
|
||||||
|
set { _viewModelChanger = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IChangeViewModel
|
||||||
|
|
||||||
|
public void PopViewModel()
|
||||||
|
{
|
||||||
|
_viewModelChanger?.PopViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PushViewModel(BaseViewModel model)
|
||||||
|
{
|
||||||
|
_viewModelChanger?.PushViewModel(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
+34
-3
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using MayShow.Helpers;
|
||||||
|
using MayShow.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MayShow.ViewModels;
|
||||||
|
|
||||||
|
class MainViewModel : ChangeNotifier, IChangeViewModel
|
||||||
|
{
|
||||||
|
BaseViewModel _currentViewModel;
|
||||||
|
Stack<BaseViewModel> _viewModels;
|
||||||
|
|
||||||
|
public MainViewModel(ITopLevelGrabber topLevelGrabber): base()
|
||||||
|
{
|
||||||
|
_viewModels = new Stack<BaseViewModel>();
|
||||||
|
var initialViewModel = new StartNewChooseReportViewModel(this)
|
||||||
|
{
|
||||||
|
TopLevelGrabber = topLevelGrabber
|
||||||
|
};
|
||||||
|
_viewModels.Push(initialViewModel);
|
||||||
|
_currentViewModel = initialViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseViewModel CurrentViewModel
|
||||||
|
{
|
||||||
|
get { return _currentViewModel; }
|
||||||
|
set { _currentViewModel = value; NotifyPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IChangeViewModel
|
||||||
|
|
||||||
|
public void PushViewModel(BaseViewModel model)
|
||||||
|
{
|
||||||
|
_viewModels.Push(model);
|
||||||
|
CurrentViewModel = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PopViewModel()
|
||||||
|
{
|
||||||
|
if (_viewModels.Count > 1)
|
||||||
|
{
|
||||||
|
_viewModels.Pop();
|
||||||
|
CurrentViewModel = _viewModels.Peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using DialogHostAvalonia;
|
||||||
|
using MayShow.Interfaces;
|
||||||
|
using MayShow.Models;
|
||||||
|
using MayShow.Helpers;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MayShow.Enums;
|
||||||
|
|
||||||
|
namespace MayShow.ViewModels;
|
||||||
|
|
||||||
|
class SettingsViewModel: ChangeNotifier
|
||||||
|
{
|
||||||
|
private Settings _previousSettings;
|
||||||
|
private Settings _settings;
|
||||||
|
private string _errorMessage;
|
||||||
|
private ITopLevelGrabber? _topLevelGrabber;
|
||||||
|
private List<DateDisplayFormat> _dateFormats;
|
||||||
|
private int _gridDisplayDateFormatSelectedIndex;
|
||||||
|
private int _reportDisplayDateFormatSelectedIndex;
|
||||||
|
|
||||||
|
public SettingsViewModel(Settings settingsToEdit, ITopLevelGrabber? topLevelGrabber): base()
|
||||||
|
{
|
||||||
|
_previousSettings = settingsToEdit;
|
||||||
|
_settings = new Settings(settingsToEdit); // clone it
|
||||||
|
_errorMessage = "";
|
||||||
|
_topLevelGrabber = topLevelGrabber;
|
||||||
|
_dateFormats = Constants.GetDateDisplayFormats();
|
||||||
|
_gridDisplayDateFormatSelectedIndex = _dateFormats.FindIndex(x => x.Value == _previousSettings.DataGridDateFormat);
|
||||||
|
if (_gridDisplayDateFormatSelectedIndex == -1)
|
||||||
|
{
|
||||||
|
_gridDisplayDateFormatSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
_reportDisplayDateFormatSelectedIndex = _dateFormats.FindIndex(x => x.Value == _previousSettings.ReportDateFormat);
|
||||||
|
if (_reportDisplayDateFormatSelectedIndex == -1)
|
||||||
|
{
|
||||||
|
_reportDisplayDateFormatSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseDocnetPDFImageRendering
|
||||||
|
{
|
||||||
|
get => _settings.UseDocnetPDFImageRendering;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_settings.UseDocnetPDFImageRendering = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SaveOutputPdfInChosenDir
|
||||||
|
{
|
||||||
|
get => PDFOutputSaveLocation == PDFSaveLocation.OtherChosenDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PDFSaveLocation PDFOutputSaveLocation
|
||||||
|
{
|
||||||
|
get => _settings.PDFOutputSaveLocation;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_settings.PDFOutputSaveLocation = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
NotifyPropertyChanged(nameof(SaveOutputPdfInChosenDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string OutputPdfDirPath
|
||||||
|
{
|
||||||
|
get => _settings.OutputPdfDir;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_settings.OutputPdfDir = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
NotifyPropertyChanged(nameof(IsOutputPdfDirValid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOutputPdfDirValid
|
||||||
|
{
|
||||||
|
get => !SaveOutputPdfInChosenDir || (SaveOutputPdfInChosenDir && Directory.Exists(OutputPdfDirPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasErrorMessage
|
||||||
|
{
|
||||||
|
get => !string.IsNullOrWhiteSpace(_errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ErrorMessage
|
||||||
|
{
|
||||||
|
get => _errorMessage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_errorMessage = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
NotifyPropertyChanged(nameof(HasErrorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal ImageResizeThreshold
|
||||||
|
{
|
||||||
|
get => _settings.ImageResizeThreshold;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_settings.ImageResizeThreshold = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DateDisplayFormat> DateFormats
|
||||||
|
{
|
||||||
|
get => _dateFormats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int DataGridDisplayDateFormatSelectedIndex
|
||||||
|
{
|
||||||
|
get => _gridDisplayDateFormatSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_gridDisplayDateFormatSelectedIndex = value;
|
||||||
|
_settings.DataGridDateFormat = _dateFormats[value].Value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReportDisplayDateFormatSelectedIndex
|
||||||
|
{
|
||||||
|
get => _reportDisplayDateFormatSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_reportDisplayDateFormatSelectedIndex = value;
|
||||||
|
_settings.ReportDateFormat = _dateFormats[value].Value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ChooseOutputFolder()
|
||||||
|
{
|
||||||
|
var topLevel = _topLevelGrabber?.GetTopLevel();
|
||||||
|
if (topLevel != null)
|
||||||
|
{
|
||||||
|
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()
|
||||||
|
{
|
||||||
|
Title = "Choose where to save your report file...",
|
||||||
|
AllowMultiple = false,
|
||||||
|
});
|
||||||
|
if (folders.Count == 1)
|
||||||
|
{
|
||||||
|
var folder = folders[0];
|
||||||
|
OutputPdfDirPath = folder.Path.LocalPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenSettingsDir()
|
||||||
|
{
|
||||||
|
var topLevel = _topLevelGrabber?.GetTopLevel();
|
||||||
|
var dirName = Utilities.GetInternalDataPath();
|
||||||
|
if (topLevel is not null && dirName != null)
|
||||||
|
{
|
||||||
|
var launcher = topLevel.Launcher;
|
||||||
|
launcher.LaunchUriAsync(new Uri(dirName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
DialogHost.Close("DialogHost", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
if (!IsOutputPdfDirValid)
|
||||||
|
{
|
||||||
|
ErrorMessage = "Output directory cannot be found!";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessage = "";
|
||||||
|
DialogHost.Close("DialogHost", _settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using DialogHostAvalonia;
|
||||||
|
using MayShow.Helpers;
|
||||||
|
using MayShow.Models;
|
||||||
|
|
||||||
|
namespace MayShow.ViewModels;
|
||||||
|
|
||||||
|
class WarningDeleteItemViewModel : ChangeNotifier
|
||||||
|
{
|
||||||
|
ReportFile _file;
|
||||||
|
|
||||||
|
public WarningDeleteItemViewModel(ReportFile file): base()
|
||||||
|
{
|
||||||
|
_file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportFile File
|
||||||
|
{
|
||||||
|
get => _file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void KeepItem()
|
||||||
|
{
|
||||||
|
DialogHost.Close("DialogHost", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveItem()
|
||||||
|
{
|
||||||
|
DialogHost.Close("DialogHost", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
MaxWidth="450">
|
MaxWidth="450">
|
||||||
<StackPanel Orientation="Vertical"
|
<StackPanel Orientation="Vertical"
|
||||||
Spacing="4">
|
Spacing="4">
|
||||||
<TextBlock Text="MayShow"
|
<TextBlock Text="MayShow 1.4.3"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
@@ -22,6 +22,18 @@
|
|||||||
<TextBlock Text="App icon made using https://gauger.me/fonticon/ with FontAwesome icon 'file-invoice-dollar' and the macOS software Icon Composer."
|
<TextBlock Text="App icon made using https://gauger.me/fonticon/ with FontAwesome icon 'file-invoice-dollar' and the macOS software Icon Composer."
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
FontSize="14"/>
|
FontSize="14"/>
|
||||||
|
<Label Content="Open Source Licenses:"/>
|
||||||
|
<ScrollViewer Margin="2"
|
||||||
|
Grid.Row="4"
|
||||||
|
x:Name="LogScrollView"
|
||||||
|
VerticalScrollBarVisibility="Visible"
|
||||||
|
AllowAutoHide="False"
|
||||||
|
MaxHeight="250">
|
||||||
|
<SelectableTextBlock Margin="2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="10"
|
||||||
|
x:Name="LicenseTextBlock"/>
|
||||||
|
</ScrollViewer>
|
||||||
<TextBlock Text="Copyright 2026 - Quickity Quack Productions"
|
<TextBlock Text="Copyright 2026 - Quickity Quack Productions"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class AboutView : UserControl
|
||||||
|
{
|
||||||
|
public AboutView()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// set license text
|
||||||
|
var processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
|
||||||
|
var licenseFileName = Path.Combine(processDir, "Assets", "LICENSES.txt");
|
||||||
|
var licenseText = "";
|
||||||
|
if (File.Exists(licenseFileName))
|
||||||
|
{
|
||||||
|
licenseText = File.ReadAllText(licenseFileName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
licenseFileName = Path.Combine(processDir, "../Resources/Assets/LICENSES.txt");
|
||||||
|
if (File.Exists(licenseFileName))
|
||||||
|
{
|
||||||
|
licenseText = File.ReadAllText(licenseFileName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
licenseText = "Error: Unable to find license file!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LicenseTextBlock.Text = licenseText.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,59 +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>
|
||||||
|
<helpers:DateFormatConverter x:Key="DateFormatter" />
|
||||||
|
</UserControl.Resources>
|
||||||
<Grid ColumnDefinitions="*"
|
<Grid ColumnDefinitions="*"
|
||||||
RowDefinitions="Auto, 2*, Auto, Auto, *">
|
RowDefinitions="Auto, 2*, Auto, Auto, *">
|
||||||
<Button Command="{Binding ShowSettings}"
|
<StackPanel Orientation="Vertical"
|
||||||
|
Spacing="2"
|
||||||
|
Margin="0,4,0,0">
|
||||||
|
<Button Command="{Binding ReturnToMainMenu}"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="4,4,0,4">
|
Margin="4,4,0,4">
|
||||||
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Settings</TextBlock>
|
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Return to Main Menu</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding ShowAbout}"
|
<Label Content="Report Title" />
|
||||||
Grid.Row="0"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,4,4,4">
|
|
||||||
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> About</TextBlock>
|
|
||||||
</Button>
|
|
||||||
<StackPanel Orientation="Vertical"
|
|
||||||
Spacing="2"
|
|
||||||
Margin="0,4,0,0">
|
|
||||||
<Label Content="MayShow: Report Builder"
|
|
||||||
FontSize="20"
|
|
||||||
FontWeight="Bold"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<Grid ColumnDefinitions="Auto, *"
|
|
||||||
Margin="4,0,0,0">
|
|
||||||
<Button Content="Choose Receipt Folder"
|
|
||||||
Command="{Binding ChooseFolder}"
|
|
||||||
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>
|
||||||
@@ -97,7 +74,7 @@
|
|||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid ColumnDefinitions="Auto, *">
|
<Grid ColumnDefinitions="Auto, *">
|
||||||
<Button Command="{Binding $parent[DataGrid].((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"
|
||||||
@@ -107,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[DataGrid].((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"
|
||||||
@@ -125,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>
|
||||||
@@ -133,19 +110,29 @@
|
|||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTemplateColumn Header="Receipt Date"
|
<DataGridTemplateColumn Header="Receipt Date"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
Width="125">
|
Width="150">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Label Content="{Binding ReceiptDate}"
|
<Label VerticalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="8,0,8,0"
|
Margin="8,0,8,0"
|
||||||
HorizontalAlignment="Left"/>
|
HorizontalAlignment="Left">
|
||||||
|
<Label.Content>
|
||||||
|
<MultiBinding Converter="{StaticResource DateFormatter}">
|
||||||
|
<Binding Path="ReceiptDate" />
|
||||||
|
<Binding Path="$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormat" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Label.Content>
|
||||||
|
</Label>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
<DataGridTemplateColumn.CellEditingTemplate>
|
<DataGridTemplateColumn.CellEditingTemplate>
|
||||||
<DataTemplate DataType="models:ReportFile">
|
<DataTemplate DataType="models:ReportFile">
|
||||||
<CalendarDatePicker SelectedDate="{Binding ReceiptDateTime}"
|
<CalendarDatePicker SelectedDate="{Binding ReceiptDateTime}"
|
||||||
DisplayDate="{Binding ReceiptDateTime}"/>
|
DisplayDate="{Binding ReceiptDateTime}"
|
||||||
|
SelectedDateFormat="Custom"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Watermark="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormatWatermark}"
|
||||||
|
CustomDateFormatString="{Binding $parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).DataGridDateFormat}"/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellEditingTemplate>
|
</DataGridTemplateColumn.CellEditingTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
@@ -156,6 +143,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding FileName}"
|
<TextBlock Text="{Binding FileName}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
ToolTip.Tip="{Binding FileName}"
|
ToolTip.Tip="{Binding FileName}"
|
||||||
Margin="8,0,8,0"
|
Margin="8,0,8,0"
|
||||||
HorizontalAlignment="Left"/>
|
HorizontalAlignment="Left"/>
|
||||||
@@ -164,25 +152,27 @@
|
|||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTemplateColumn Header=""
|
<DataGridTemplateColumn Header=""
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
Width="*">
|
Width="200">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Horizontal"
|
<StackPanel Orientation="Horizontal"
|
||||||
Spacing="4">
|
Spacing="4">
|
||||||
<Button Command="{Binding $parent[DataGrid].((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"
|
||||||
IsEnabled="{Binding !$parent[DataGrid].((vm:MainViewModel)DataContext).IsCreatingPDF}">
|
FontSize="12"
|
||||||
|
IsEnabled="{Binding !$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).IsCreatingPDF}">
|
||||||
<Button.Content>
|
<Button.Content>
|
||||||
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock>
|
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Edit</TextBlock>
|
||||||
</Button.Content>
|
</Button.Content>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding $parent[DataGrid].((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"
|
||||||
IsEnabled="{Binding !$parent[DataGrid].((vm:MainViewModel)DataContext).IsCreatingPDF}">
|
FontSize="12"
|
||||||
|
IsEnabled="{Binding !$parent[UserControl].((vm:CreatePDFReportViewModel)DataContext).IsCreatingPDF}">
|
||||||
<Button.Content>
|
<Button.Content>
|
||||||
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Remove</TextBlock>
|
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Remove</TextBlock>
|
||||||
</Button.Content>
|
</Button.Content>
|
||||||
@@ -206,13 +196,13 @@
|
|||||||
Spacing="8"
|
Spacing="8"
|
||||||
Margin="4"
|
Margin="4"
|
||||||
Grid.Row="2">
|
Grid.Row="2">
|
||||||
<Button Command="{Binding $parent[DataGrid].((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="" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File Location</TextBlock>
|
<TextBlock FontSize="12"><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File Location</TextBlock>
|
||||||
</Button.Content>
|
</Button.Content>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding $parent[DataGrid].((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="" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock>
|
<TextBlock FontSize="12"><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Open File</TextBlock>
|
||||||
@@ -232,7 +222,7 @@
|
|||||||
Spacing="4">
|
Spacing="4">
|
||||||
<Button Command="{Binding AddItem}"
|
<Button Command="{Binding AddItem}"
|
||||||
IsEnabled="{Binding CanAddItem}">
|
IsEnabled="{Binding CanAddItem}">
|
||||||
<TextBlock><Run Text="+" FontFamily="{StaticResource FontAwesomeSolid}"/> Add Item(s)</TextBlock>
|
<TextBlock><Run Text="+" FontFamily="{StaticResource FontAwesomeSolid}"/> Add Item(s)...</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Command="{Binding RemoveAllItems}"
|
<Button Command="{Binding RemoveAllItems}"
|
||||||
IsEnabled="{Binding IsCreatePDFButtonEnabled}"
|
IsEnabled="{Binding IsCreatePDFButtonEnabled}"
|
||||||
@@ -244,7 +234,8 @@
|
|||||||
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Re-sort PDF Items</TextBlock>
|
<TextBlock><Run Text="" 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="" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock>
|
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Save Report Info</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -270,14 +261,23 @@
|
|||||||
Spacing="2"
|
Spacing="2"
|
||||||
Grid.Row="3">
|
Grid.Row="3">
|
||||||
<Rectangle Fill="Gray" Height="3" HorizontalAlignment="Stretch"/>
|
<Rectangle Fill="Gray" Height="3" HorizontalAlignment="Stretch"/>
|
||||||
<Label Content="Program Log" FontSize="14" FontWeight="Bold"/>
|
<Grid ColumnDefinitions="Auto, *">
|
||||||
|
<Label Content="Program Log" FontSize="14" FontWeight="Bold" Grid.Column="0"/>
|
||||||
|
<Button Command="{Binding CopyLogToClipboard}"
|
||||||
|
FontSize="10"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,2,8,2">
|
||||||
|
<TextBlock><Run Text="" FontFamily="{StaticResource FontAwesomeSolid}"/> Copy Program Log to Clipboard</TextBlock>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ScrollViewer Margin="2"
|
<ScrollViewer Margin="2"
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
x:Name="LogScrollView"
|
x:Name="LogScrollView"
|
||||||
VerticalScrollBarVisibility="Visible"
|
VerticalScrollBarVisibility="Visible"
|
||||||
AllowAutoHide="False">
|
AllowAutoHide="False">
|
||||||
<SelectableTextBlock Text="{Binding CreatePDFLog}"
|
<SelectableTextBlock Text="{Binding ProgramLog}"
|
||||||
Margin="2"
|
Margin="2"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
x:Name="LogBlock"/>
|
x:Name="LogBlock"/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using MayShow.ViewModels;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class CreatePDFReportView : UserControl
|
||||||
|
{
|
||||||
|
public CreatePDFReportView()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
LogBlock.PropertyChanged += LogBlock_PropertyChanged;
|
||||||
|
FilesGrid.CellEditEnded += FileCellEditEnded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogBlock_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Property.ToString() == "Text")
|
||||||
|
{
|
||||||
|
LogScrollView.ScrollToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnfocusTextbox()
|
||||||
|
{
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
topLevel?.FocusManager?.ClearFocus();
|
||||||
|
if (DataContext is CreatePDFReportViewModel mvm)
|
||||||
|
{
|
||||||
|
mvm?.HasUnsavedWork = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FileCellEditEnded(object? sender, DataGridCellEditEndedEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.EditAction == DataGridEditAction.Commit && DataContext is CreatePDFReportViewModel mvm)
|
||||||
|
{
|
||||||
|
mvm?.HasUnsavedWork = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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=""
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" /> Cancel</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding Save}"
|
||||||
|
Classes="accent">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text=""
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" /> Save</TextBlock>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class EditFile : UserControl
|
||||||
|
{
|
||||||
|
public EditFile()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<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.SettingsView"
|
||||||
|
xmlns:models="clr-namespace:MayShow.Models"
|
||||||
|
xmlns:enums="clr-namespace:MayShow.Enums"
|
||||||
|
xmlns:vm="clr-namespace:MayShow.ViewModels"
|
||||||
|
xmlns:converters="using:Avalonia.Controls.Converters"
|
||||||
|
x:DataType="vm:SettingsViewModel"
|
||||||
|
MaxWidth="450">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
<ScrollViewer AllowAutoHide="False">
|
||||||
|
<StackPanel Orientation="Vertical"
|
||||||
|
Spacing="8"
|
||||||
|
Margin="12,4,12,4">
|
||||||
|
<Label Content="Settings"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="Bold" />
|
||||||
|
<TextBlock TextWrapping="Wrap"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Width="350">
|
||||||
|
Always reduce size of images if above this file size in megabytes (MB):
|
||||||
|
</TextBlock>
|
||||||
|
<NumericUpDown Value="{Binding ImageResizeThreshold}"
|
||||||
|
Increment="0.1"
|
||||||
|
Minimum="0.5"
|
||||||
|
FormatString="0.00"
|
||||||
|
Maximum="10"
|
||||||
|
Width="150"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="0,0,0,4" />
|
||||||
|
<CheckBox IsChecked="{Binding !UseDocnetPDFImageRendering}">Use legacy PDF handling (does not work with macOS annotations)</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, *"
|
||||||
|
IsVisible="{Binding SaveOutputPdfInChosenDir}">
|
||||||
|
<Button Content="Choose PDF report output folder..."
|
||||||
|
Grid.Column="0"
|
||||||
|
Command="{Binding ChooseOutputFolder}"
|
||||||
|
VerticalAlignment="Top"/>
|
||||||
|
<TextBlock Text="{Binding OutputPdfDirPath}"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"/>
|
||||||
|
</Grid>
|
||||||
|
<Label Content="File List Date Format"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
FontSize="14" />
|
||||||
|
<ComboBox SelectedIndex="{Binding DataGridDisplayDateFormatSelectedIndex}"
|
||||||
|
ItemsSource="{Binding DateFormats}"
|
||||||
|
MaxDropDownHeight="300">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:DateDisplayFormat">
|
||||||
|
<TextBlock Text="{Binding Example}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
<Label Content="PDF Report Date Format"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
FontSize="14" />
|
||||||
|
<ComboBox SelectedIndex="{Binding ReportDisplayDateFormatSelectedIndex}"
|
||||||
|
ItemsSource="{Binding DateFormats}"
|
||||||
|
MaxDropDownHeight="300">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:DateDisplayFormat">
|
||||||
|
<TextBlock Text="{Binding Example}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
<Button Command="{Binding OpenSettingsDir}">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text=""
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" /> Open MayShow Settings Directory</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<TextBlock TextWrapping="Wrap"
|
||||||
|
Foreground="Red"
|
||||||
|
Text="{Binding ErrorMessage}"
|
||||||
|
IsVisible="{Binding HasErrorMessage}"/>
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="12"
|
||||||
|
Margin="0,4,0,0"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button Command="{Binding Cancel}">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text=""
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" /> Cancel</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding Save}"
|
||||||
|
Classes="accent">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text=""
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" /> Save</TextBlock>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class SettingsView : UserControl
|
||||||
|
{
|
||||||
|
public SettingsView()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -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="" 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="" 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=""
|
||||||
|
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=""
|
||||||
|
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=""
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class WarningDeleteItem : UserControl
|
||||||
|
{
|
||||||
|
public WarningDeleteItem()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
MaxWidth="350"
|
MaxWidth="400"
|
||||||
Text="{Binding Error}"/>
|
Text="{Binding Error}"/>
|
||||||
<Button Command="{Binding Close}"
|
<Button Command="{Binding Close}"
|
||||||
Classes="accent"
|
Classes="accent"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace MayShow.Views;
|
||||||
|
|
||||||
|
public partial class WarningView : UserControl
|
||||||
|
{
|
||||||
|
public WarningView()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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(); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MayShow.Helpers;
|
|
||||||
using MayShows.Helpers;
|
|
||||||
|
|
||||||
namespace MayShow.Models;
|
|
||||||
|
|
||||||
class Settings : ChangeNotifier
|
|
||||||
{
|
|
||||||
private string _lastUsedPath;
|
|
||||||
private bool _useDocnetPDFImageRendering;
|
|
||||||
|
|
||||||
public Settings()
|
|
||||||
{
|
|
||||||
_lastUsedPath = "";
|
|
||||||
_useDocnetPDFImageRendering = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonInclude]
|
|
||||||
public string LastUsedPath
|
|
||||||
{
|
|
||||||
get => _lastUsedPath;
|
|
||||||
set { _lastUsedPath = value; NotifyPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonInclude]
|
|
||||||
public bool UseDocnetPFDImageRendering
|
|
||||||
{
|
|
||||||
get => _useDocnetPDFImageRendering;
|
|
||||||
set { _useDocnetPDFImageRendering = value; NotifyPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetSettingsFileName()
|
|
||||||
{
|
|
||||||
return "settings.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetSettingsPath()
|
|
||||||
{
|
|
||||||
var path = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
||||||
"MayShow"
|
|
||||||
);
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
}
|
|
||||||
return Path.Combine(path, GetSettingsFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using MayShow.Helpers;
|
|
||||||
using MayShow.Interfaces;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MayShow.ViewModels
|
|
||||||
{
|
|
||||||
class BaseViewModel : ChangeNotifier
|
|
||||||
{
|
|
||||||
IChangeViewModel _viewModelChanger;
|
|
||||||
ITopLevelGrabber? _topLevelGrabber;
|
|
||||||
|
|
||||||
public BaseViewModel(IChangeViewModel viewModelChanger)
|
|
||||||
{
|
|
||||||
_viewModelChanger = viewModelChanger;
|
|
||||||
_topLevelGrabber = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITopLevelGrabber? TopLevelGrabber
|
|
||||||
{
|
|
||||||
get => _topLevelGrabber;
|
|
||||||
set { _topLevelGrabber = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public IChangeViewModel ViewModelChanger
|
|
||||||
{
|
|
||||||
get { return _viewModelChanger; }
|
|
||||||
set { _viewModelChanger = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IChangeViewModel
|
|
||||||
|
|
||||||
public void PopViewModel()
|
|
||||||
{
|
|
||||||
_viewModelChanger?.PopViewModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PushViewModel(BaseViewModel model)
|
|
||||||
{
|
|
||||||
_viewModelChanger?.PushViewModel(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,868 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
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.Helpers;
|
|
||||||
using MayShow.Interfaces;
|
|
||||||
using MayShow.Models;
|
|
||||||
using MayShows.Helpers;
|
|
||||||
|
|
||||||
using Docnet.Core.Models;
|
|
||||||
using Docnet.Core;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using System.Reflection.Metadata.Ecma335;
|
|
||||||
using Docnet.Core.Readers;
|
|
||||||
|
|
||||||
namespace MayShow.ViewModels;
|
|
||||||
|
|
||||||
class MainViewModel : BaseViewModel, IFontResolver, ICanCheckShutdown
|
|
||||||
{
|
|
||||||
private bool _isPerformingInitialLoad;
|
|
||||||
private string _processDir;
|
|
||||||
private bool _isCreatingPDF;
|
|
||||||
private string _createPDFLog;
|
|
||||||
private string _workingFolder;
|
|
||||||
|
|
||||||
private string _reportTitle;
|
|
||||||
private ObservableCollection<ReportFile> _reportFiles;
|
|
||||||
private DateTime? _lastGeneratedTime;
|
|
||||||
|
|
||||||
private Settings _settings;
|
|
||||||
|
|
||||||
private bool _hasUnsavedWork;
|
|
||||||
|
|
||||||
public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
|
|
||||||
{
|
|
||||||
_isPerformingInitialLoad = true;
|
|
||||||
_processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
|
|
||||||
Console.WriteLine("Process is running from: {0}", _processDir);
|
|
||||||
_isCreatingPDF = false;
|
|
||||||
var quotes = Constants.GetQuotes();
|
|
||||||
Random random = new Random();
|
|
||||||
var quoteIndex = random.Next(0, quotes.Length);
|
|
||||||
_createPDFLog = "----- MayShow v" + Constants.AppVersion + " ------" + Environment.NewLine;
|
|
||||||
_createPDFLog += quotes[quoteIndex] + Environment.NewLine;
|
|
||||||
_createPDFLog += "---------------------------------------" + Environment.NewLine;
|
|
||||||
_createPDFLog += "Loaded and ready to create report!" + Environment.NewLine;
|
|
||||||
_createPDFLog += "Please copy and send this Program Log when reporting any issues with the software.";
|
|
||||||
_workingFolder = "";
|
|
||||||
ReportFiles = _reportFiles = new ObservableCollection<ReportFile>();
|
|
||||||
_reportTitle = "";
|
|
||||||
_lastGeneratedTime = null;
|
|
||||||
_settings = Settings.LoadSettings();
|
|
||||||
if (!string.IsNullOrWhiteSpace(_settings.LastUsedPath))
|
|
||||||
{
|
|
||||||
LogInfo("Loading data at last used path of {0}", _settings.LastUsedPath);
|
|
||||||
ScanFolder(_settings.LastUsedPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LogInfo("Choose a receipt folder to begin...");
|
|
||||||
}
|
|
||||||
HasUnsavedWork = false;
|
|
||||||
_isPerformingInitialLoad = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ReportTitle
|
|
||||||
{
|
|
||||||
get => _reportTitle;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_reportTitle = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
NotifyPropertyChanged(nameof(IsTitleBoxVisible));
|
|
||||||
NotifyPropertyChanged(nameof(CanAddItem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsTitleBoxVisible
|
|
||||||
{
|
|
||||||
get => !string.IsNullOrWhiteSpace(_workingFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanAddItem
|
|
||||||
{
|
|
||||||
get => IsTitleBoxVisible && !IsCreatingPDF;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCreatingPDF
|
|
||||||
{
|
|
||||||
get => _isCreatingPDF;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_isCreatingPDF = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
|
|
||||||
NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF));
|
|
||||||
NotifyPropertyChanged(nameof(CanAddItem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCreatePDFButtonEnabled
|
|
||||||
{
|
|
||||||
get => !_isCreatingPDF && _reportFiles.Count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasWorkingFolder
|
|
||||||
{
|
|
||||||
get => !string.IsNullOrWhiteSpace(_workingFolder) && Directory.Exists(_workingFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasWorkingFolderAndNotMakingPDF
|
|
||||||
{
|
|
||||||
get => !string.IsNullOrWhiteSpace(_workingFolder) && Directory.Exists(_workingFolder) && !_isCreatingPDF;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WorkingFolder
|
|
||||||
{
|
|
||||||
get => _workingFolder;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_workingFolder = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
NotifyPropertyChanged(nameof(HasWorkingFolder));
|
|
||||||
NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreatePDFLog
|
|
||||||
{
|
|
||||||
get => _createPDFLog;
|
|
||||||
set { _createPDFLog = value; NotifyPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasUnsavedWork
|
|
||||||
{
|
|
||||||
get => _hasUnsavedWork;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_hasUnsavedWork = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<ReportFile> ReportFiles
|
|
||||||
{
|
|
||||||
get => _reportFiles;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_reportFiles = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
_reportFiles.CollectionChanged += ( sender, e ) =>
|
|
||||||
{
|
|
||||||
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
|
|
||||||
HasUnsavedWork = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogInfo(string message, params object[]? arguments)
|
|
||||||
{
|
|
||||||
var timestamp = string.Format("[{0:s}]", DateTime.Now);
|
|
||||||
Console.WriteLine(timestamp + " " + message, arguments);
|
|
||||||
CreatePDFLog += 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 void ScanFolder(string path)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
WorkingFolder = path;
|
|
||||||
NotifyPropertyChanged(nameof(IsTitleBoxVisible));
|
|
||||||
NotifyPropertyChanged(nameof(CanAddItem));
|
|
||||||
var reportFilePath = Path.Combine(path, GetReportSavedDataFileName());
|
|
||||||
var successfullyLoadedPriorReport = false;
|
|
||||||
if (File.Exists(reportFilePath))
|
|
||||||
{
|
|
||||||
// load prior report
|
|
||||||
var json = File.ReadAllText(reportFilePath);
|
|
||||||
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
|
|
||||||
var report = JsonSerializer.Deserialize<PDFReport>(json, jsonContext.PDFReport);
|
|
||||||
if (report != null && report.Files.Count > 0)
|
|
||||||
{
|
|
||||||
ReportFiles = new ObservableCollection<ReportFile>(report.Files);
|
|
||||||
ReportTitle = report.Title;
|
|
||||||
WorkingFolder = report.BaseFolder;
|
|
||||||
_lastGeneratedTime = report.LastGenerated ?? null;
|
|
||||||
LogInfo("Reloaded report last saved at {0}", report.LastSaved);
|
|
||||||
successfullyLoadedPriorReport = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!successfullyLoadedPriorReport)
|
|
||||||
{
|
|
||||||
// Scan folder for files and display in DataGrid
|
|
||||||
ReportFiles.Clear();
|
|
||||||
ReportTitle = "";
|
|
||||||
var filePaths = Directory.GetFiles(_workingFolder);
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowAbout()
|
|
||||||
{
|
|
||||||
DialogHost.Show(new AboutViewModel());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ShowSettings()
|
|
||||||
{
|
|
||||||
var updatedSettings = await DialogHost.Show(new SettingsViewModel(_settings));
|
|
||||||
if (updatedSettings != null)
|
|
||||||
{
|
|
||||||
_settings = (Settings)updatedSettings;
|
|
||||||
await _settings.SaveSettingsAsync();
|
|
||||||
LogInfo("Saved updated settings!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = [
|
|
||||||
new FilePickerFileType("All Types")
|
|
||||||
{
|
|
||||||
Patterns = Constants.AllowedFileExtensionPatterns,
|
|
||||||
AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
|
|
||||||
MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
|
|
||||||
},
|
|
||||||
FilePickerFileTypes.ImageAll,
|
|
||||||
new FilePickerFileType("HEIC Images")
|
|
||||||
{
|
|
||||||
Patterns = [ "*.heic" ],
|
|
||||||
AppleUniformTypeIdentifiers = [ "public.heic" ],
|
|
||||||
MimeTypes = [ "image/heic" ]
|
|
||||||
},
|
|
||||||
FilePickerFileTypes.Pdf,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
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(GetReportSavedDataFileName()))
|
|
||||||
{
|
|
||||||
LogInfo("File {0} did not match allowed file extension types, so it was not added.", filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var date = Utilities.CheckValidDateInString(filePath);
|
|
||||||
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 async void RemoveAllItems()
|
|
||||||
{
|
|
||||||
var result = await DialogHost.Show(new ConfirmViewModel("Warning!", "Are you sure you want to remove all items from this report?", "Remove All Items", "Cancel"));
|
|
||||||
if (result != null && (bool)result)
|
|
||||||
{
|
|
||||||
ReportFiles.Clear();
|
|
||||||
HasUnsavedWork = true;
|
|
||||||
NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LocateFile(object f) => LocateFileImpl((ReportFile) f);
|
|
||||||
public 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 = [
|
|
||||||
new FilePickerFileType("All Types")
|
|
||||||
{
|
|
||||||
Patterns = Constants.AllowedFileExtensionPatterns,
|
|
||||||
AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
|
|
||||||
MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
|
|
||||||
},
|
|
||||||
FilePickerFileTypes.ImageAll,
|
|
||||||
new FilePickerFileType("HEIC Images")
|
|
||||||
{
|
|
||||||
Patterns = [ "*.heic" ],
|
|
||||||
AppleUniformTypeIdentifiers = [ "public.heic" ],
|
|
||||||
MimeTypes = [ "image/heic" ]
|
|
||||||
},
|
|
||||||
FilePickerFileTypes.Pdf,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
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));
|
|
||||||
HasUnsavedWork = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void BuildPDF()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(ReportTitle))
|
|
||||||
{
|
|
||||||
await DialogHost.Show(new WarningViewModel("You must provide a report title!"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Run(() => CreatePDF(_workingFolder));
|
|
||||||
} catch (Exception e)
|
|
||||||
{
|
|
||||||
LogInfo("PDF process failed! Reason: " + e.Message);
|
|
||||||
if (e.StackTrace != null)
|
|
||||||
{
|
|
||||||
LogInfo(e.StackTrace);
|
|
||||||
}
|
|
||||||
if (e.InnerException != null)
|
|
||||||
{
|
|
||||||
LogInfo("Inner exception: " + e.InnerException.Message);
|
|
||||||
if (e.InnerException.StackTrace != null)
|
|
||||||
{
|
|
||||||
LogInfo(e.InnerException.StackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogInfo("Please report this error to a programmer or fix the issue listed above.");
|
|
||||||
IsCreatingPDF = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveInterimReportInfo()
|
|
||||||
{
|
|
||||||
var report = new PDFReport()
|
|
||||||
{
|
|
||||||
Title = ReportTitle,
|
|
||||||
Files = ReportFiles.ToList(),
|
|
||||||
BaseFolder = _workingFolder,
|
|
||||||
LastSaved = DateTime.Now,
|
|
||||||
LastGenerated = _lastGeneratedTime,
|
|
||||||
};
|
|
||||||
await SavePDFReportDataToDisk(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SavePDFReportDataToDisk(PDFReport report)
|
|
||||||
{
|
|
||||||
var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
await JsonSerializer.SerializeAsync(memoryStream, report, jsonContext.PDFReport);
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
using var reader = new StreamReader(memoryStream);
|
|
||||||
var json = await reader.ReadToEndAsync();
|
|
||||||
var savePath = Path.Combine(_workingFolder, GetReportSavedDataFileName());
|
|
||||||
await File.WriteAllTextAsync(savePath, json);
|
|
||||||
LogInfo("Saved report information to {0}", savePath);
|
|
||||||
HasUnsavedWork = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CreateAndSaveReportObjectAfterReportCreation()
|
|
||||||
{
|
|
||||||
var report = new PDFReport()
|
|
||||||
{
|
|
||||||
Title = ReportTitle,
|
|
||||||
Files = ReportFiles.ToList(),
|
|
||||||
BaseFolder = _workingFolder,
|
|
||||||
LastSaved = DateTime.Now,
|
|
||||||
LastGenerated = DateTime.Now,
|
|
||||||
};
|
|
||||||
_lastGeneratedTime = DateTime.Now;
|
|
||||||
await SavePDFReportDataToDisk(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetReportSavedDataFileName()
|
|
||||||
{
|
|
||||||
return "report_data.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[]? GetFont(string faceName)
|
|
||||||
{
|
|
||||||
LogInfo(string.Format("Loading font {0}", faceName));
|
|
||||||
if (faceName == "Noto Sans JP")
|
|
||||||
{
|
|
||||||
var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
path = Path.Combine(_processDir, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
|
|
||||||
}
|
|
||||||
return File.ReadAllBytes(path);
|
|
||||||
}
|
|
||||||
if (faceName == "Noto Sans JP Bold")
|
|
||||||
{
|
|
||||||
var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
path = Path.Combine(_processDir, "../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 JP")
|
|
||||||
{
|
|
||||||
if (bold)
|
|
||||||
{
|
|
||||||
return new FontResolverInfo(familyName + " Bold");
|
|
||||||
}
|
|
||||||
return new FontResolverInfo(familyName);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://forum.pdfsharp.net/viewtopic.php?f=2&t=1025
|
|
||||||
private async Task CreatePDF(string folderPath)
|
|
||||||
{
|
|
||||||
// TODO: calculate needed width for images based on page width and margins and all that?
|
|
||||||
// TODO: resize (non-HEIC) images for smaller size...?
|
|
||||||
IsCreatingPDF = true;
|
|
||||||
var pdfDoc = new Document();
|
|
||||||
var outputFileName = ReportTitle + ".pdf";
|
|
||||||
var folderName = new DirectoryInfo(folderPath).Name;
|
|
||||||
const int imageWidth = 425;
|
|
||||||
if (folderName.Contains('-'))
|
|
||||||
{
|
|
||||||
// see if year/month format
|
|
||||||
var parts = folderName.Split('-');
|
|
||||||
if (parts[0].Length == 4 &&
|
|
||||||
parts[1].Length <= 2 &&
|
|
||||||
int.TryParse(parts[0], out int year) && int.TryParse(parts[1], out int month))
|
|
||||||
{
|
|
||||||
outputFileName = string.Format("{0} {1} Receipts.pdf",
|
|
||||||
CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month),
|
|
||||||
year);
|
|
||||||
LogInfo("Auto-changed output file name to " + outputFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var section = pdfDoc.AddSection();
|
|
||||||
section.PageSetup.PageFormat = PageFormat.Letter;
|
|
||||||
section.PageSetup.PageWidth = "8.5in";
|
|
||||||
section.PageSetup.PageHeight = "11in";
|
|
||||||
section.PageSetup.TopMargin = "0.5in";
|
|
||||||
section.PageSetup.RightMargin = "0.5in";
|
|
||||||
section.PageSetup.BottomMargin = "0.5in";
|
|
||||||
section.PageSetup.LeftMargin = "0.5in";
|
|
||||||
// setup footer for page number
|
|
||||||
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"));
|
|
||||||
section.Footers.Primary.Add(footerPar);
|
|
||||||
// add report title
|
|
||||||
var reportTitlePar = section.AddParagraph();
|
|
||||||
reportTitlePar.Format.Alignment = ParagraphAlignment.Center;
|
|
||||||
reportTitlePar.Format.Font.Size = 16;
|
|
||||||
reportTitlePar.Format.Font.Bold = true;
|
|
||||||
reportTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
|
|
||||||
reportTitlePar.AddText(ReportTitle);
|
|
||||||
//
|
|
||||||
var convertedDir = Path.Combine(folderPath, "converted");
|
|
||||||
if (!Directory.Exists(convertedDir))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(convertedDir);
|
|
||||||
}
|
|
||||||
//
|
|
||||||
GlobalFontSettings.FontResolver = this;
|
|
||||||
GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
IsCreatingPDF = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fileName == ".DS_Store" || fileName == outputFileName)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (i > 0 && hasAddedData)
|
|
||||||
{
|
|
||||||
section.AddPageBreak();
|
|
||||||
}
|
|
||||||
var imageTitlePar = section.AddParagraph();
|
|
||||||
imageTitlePar.Format.Alignment = ParagraphAlignment.Center;
|
|
||||||
imageTitlePar.Format.Font.Size = 12;
|
|
||||||
imageTitlePar.Format.Font.Bold = true;
|
|
||||||
imageTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
|
|
||||||
imageTitlePar.AddText(string.IsNullOrWhiteSpace(file.Title) ? file.FileName : file.Title);
|
|
||||||
var receiptDatePar = section.AddParagraph();
|
|
||||||
receiptDatePar.Format.Alignment = ParagraphAlignment.Center;
|
|
||||||
receiptDatePar.Format.Font.Size = 12;
|
|
||||||
receiptDatePar.Format.Font.Bold = true;
|
|
||||||
receiptDatePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
|
|
||||||
receiptDatePar.AddText(file.ReceiptDate.ToString("yyyy-MM-dd"));
|
|
||||||
if (!string.IsNullOrWhiteSpace(file.Notes))
|
|
||||||
{
|
|
||||||
var imageNotesPar = section.AddParagraph();
|
|
||||||
imageNotesPar.Format.Alignment = ParagraphAlignment.Center;
|
|
||||||
imageNotesPar.Format.Font.Size = 10;
|
|
||||||
imageNotesPar.Format.Font.Bold = false;
|
|
||||||
imageNotesPar.Format.Font.Name = "Noto Sans JP";
|
|
||||||
imageNotesPar.AddText(file.Notes);
|
|
||||||
}
|
|
||||||
section.AddParagraph(); // add empty line for spacing
|
|
||||||
// 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;
|
|
||||||
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;
|
|
||||||
LogInfo("Image orientation of {0} is {1}", fileName, mImage.Orientation);
|
|
||||||
if (mImage.Orientation != OrientationType.TopLeft)
|
|
||||||
{
|
|
||||||
LogInfo("Auto-adjusted image orientation of {0}", fileName);
|
|
||||||
mImage.AutoOrient();
|
|
||||||
didAdjust = true;
|
|
||||||
}
|
|
||||||
// perform needed image manipulations
|
|
||||||
if (isHEIC || isWebp || isPNG || (!isPDF && info.Length > 1.5 * 1024 * 1024 /* 1.5 MB */))
|
|
||||||
{
|
|
||||||
// Save image as jpg
|
|
||||||
mImage.Quality = 80;
|
|
||||||
if (mImage.Width >= 400 || mImage.Height >= 400)
|
|
||||||
{
|
|
||||||
loadedImageWidth = (uint)Math.Floor(mImage.Width * 0.5);
|
|
||||||
loadedImageHeight = (uint)Math.Floor(mImage.Height * 0.5);
|
|
||||||
mImage.Scale(loadedImageWidth, loadedImageHeight);
|
|
||||||
LogInfo("Image {2} scaled to {0}x{1}", loadedImageWidth, loadedImageHeight, fileName);
|
|
||||||
}
|
|
||||||
didAdjust = true;
|
|
||||||
LogInfo("Converted image {0} to JPEG", fileName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// load height/width
|
|
||||||
loadedImageWidth = mImage.Width;
|
|
||||||
loadedImageHeight = mImage.Height;
|
|
||||||
}
|
|
||||||
if (didAdjust)
|
|
||||||
{
|
|
||||||
await mImage.WriteAsync(convertedOutputPath);
|
|
||||||
filePath = convertedOutputPath;
|
|
||||||
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.LockAspectRatio = true;
|
|
||||||
if (!isPDF && loadedImageHeight > 600)
|
|
||||||
{
|
|
||||||
image.Height = 550; // make sure it will fit on one page
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
image.Width = imageWidth; // can't be too wide now...not sure why...maybe due to margins...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// need to render PDF to images
|
|
||||||
if (_settings.UseDocnetPFDImageRendering)
|
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
var image = paragraph.AddImage(convertedPdfImagePath);
|
|
||||||
image.Width = imageWidth;
|
|
||||||
image.LockAspectRatio = true;
|
|
||||||
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 = imageWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 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 = imageWidth; // can't be too wide now...not sure why...maybe due to margins...
|
|
||||||
// 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 = imageWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogInfo(string.Format("Added image: {0} ({1})", file.Title, filePath));
|
|
||||||
hasAddedData = true;
|
|
||||||
}
|
|
||||||
var pdfRenderer = new PdfDocumentRenderer
|
|
||||||
{
|
|
||||||
Document = pdfDoc,
|
|
||||||
WorkingDirectory = folderPath
|
|
||||||
};
|
|
||||||
LogInfo("Rendering document to PDF file...");
|
|
||||||
pdfRenderer.RenderDocument();
|
|
||||||
string outputPDFFileName = Path.Join(folderPath, outputFileName);
|
|
||||||
LogInfo("Saving PDF document to disk...");
|
|
||||||
pdfRenderer.PdfDocument.Save(outputPDFFileName);
|
|
||||||
LogInfo("Finished saving PDF output to: " + outputPDFFileName);
|
|
||||||
await CreateAndSaveReportObjectAfterReportCreation();
|
|
||||||
OpenFolderForFileInFileViewer(outputPDFFileName);
|
|
||||||
IsCreatingPDF = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CheckIsSafeToShutdown()
|
|
||||||
{
|
|
||||||
if (!HasUnsavedWork || string.IsNullOrWhiteSpace(WorkingFolder))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var result = await DialogHost.Show(new ShutdownCheckViewModel());
|
|
||||||
if (result != null && result is ShutdownCheckOptions opt)
|
|
||||||
{
|
|
||||||
if (opt == ShutdownCheckOptions.SaveAndShutdown)
|
|
||||||
{
|
|
||||||
await SaveInterimReportInfo();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (opt == ShutdownCheckOptions.NoSaveShutdown)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (opt == ShutdownCheckOptions.CancelShutdown)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using MayShow.Helpers;
|
|
||||||
using MayShow.Interfaces;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MayShow.ViewModels
|
|
||||||
{
|
|
||||||
class MainWindowViewModel : ChangeNotifier, IChangeViewModel
|
|
||||||
{
|
|
||||||
BaseViewModel _currentViewModel;
|
|
||||||
Stack<BaseViewModel> _viewModels;
|
|
||||||
|
|
||||||
public MainWindowViewModel(ITopLevelGrabber topLevelGrabber)
|
|
||||||
{
|
|
||||||
_viewModels = new Stack<BaseViewModel>();
|
|
||||||
var initialViewModel = new MainViewModel(this)
|
|
||||||
{
|
|
||||||
TopLevelGrabber = topLevelGrabber
|
|
||||||
};
|
|
||||||
_viewModels.Push(initialViewModel);
|
|
||||||
_currentViewModel = initialViewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BaseViewModel CurrentViewModel
|
|
||||||
{
|
|
||||||
get { return _currentViewModel; }
|
|
||||||
set { _currentViewModel = value; NotifyPropertyChanged(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IChangeViewModel
|
|
||||||
|
|
||||||
public void PushViewModel(BaseViewModel model)
|
|
||||||
{
|
|
||||||
_viewModels.Push(model);
|
|
||||||
CurrentViewModel = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PopViewModel()
|
|
||||||
{
|
|
||||||
if (_viewModels.Count > 1)
|
|
||||||
{
|
|
||||||
_viewModels.Pop();
|
|
||||||
CurrentViewModel = _viewModels.Peek();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +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;
|
|
||||||
using MayShow.Helpers;
|
|
||||||
|
|
||||||
namespace MayShow.ViewModels;
|
|
||||||
|
|
||||||
class SettingsViewModel: ChangeNotifier
|
|
||||||
{
|
|
||||||
private Settings _previousSettings;
|
|
||||||
private Settings _settings;
|
|
||||||
|
|
||||||
public SettingsViewModel(Settings settingsToEdit)
|
|
||||||
{
|
|
||||||
_previousSettings = settingsToEdit;
|
|
||||||
_settings = new Settings
|
|
||||||
{
|
|
||||||
LastUsedPath = _previousSettings.LastUsedPath,
|
|
||||||
UseDocnetPFDImageRendering = _previousSettings.UseDocnetPFDImageRendering
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseDocnetPDFImageRendering
|
|
||||||
{
|
|
||||||
get => _settings.UseDocnetPFDImageRendering;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_settings.UseDocnetPFDImageRendering = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel()
|
|
||||||
{
|
|
||||||
DialogHost.Close("DialogHost", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
DialogHost.Close("DialogHost", _settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using DialogHostAvalonia;
|
|
||||||
using MayShow.Helpers;
|
|
||||||
using MayShow.Models;
|
|
||||||
|
|
||||||
namespace MayShow.ViewModels
|
|
||||||
{
|
|
||||||
class WarningDeleteItemViewModel : ChangeNotifier
|
|
||||||
{
|
|
||||||
ReportFile _file;
|
|
||||||
|
|
||||||
public WarningDeleteItemViewModel(ReportFile file)
|
|
||||||
{
|
|
||||||
_file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReportFile File
|
|
||||||
{
|
|
||||||
get => _file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void KeepItem()
|
|
||||||
{
|
|
||||||
DialogHost.Close("DialogHost", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveItem()
|
|
||||||
{
|
|
||||||
DialogHost.Close("DialogHost", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace MayShow.Views
|
|
||||||
{
|
|
||||||
public partial class AboutView : UserControl
|
|
||||||
{
|
|
||||||
public AboutView()
|
|
||||||
{
|
|
||||||
this.InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user