Files
MayShow/src/MayShow.Shared/ViewModels/CreatePDFReportViewModel.cs
T

621 lines
21 KiB
C#

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);
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)
{
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
{
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,
});
if (result.File is not null)
{
var path = result.File.Path.AbsolutePath;
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;
}
}