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 _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 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( 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 DeterminePDFSaveLocation() { var fileName = ReportTitle + ".pdf"; switch (_settings.PDFOutputSaveLocation) { case PDFSaveLocation.BaseFolder: return Path.Combine(_pdfReport.BaseFolder, fileName); case PDFSaveLocation.AlwaysAsk: Func> 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 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; } }