diff --git a/Changelog.md b/Changelog.md index 8b095c6b..6b9916b1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,18 @@ # CHANGELOG - ## COMPASS v1.8.8 (28 June 2025) + ## COMPASS v1.8.9 (26 October 2025) + +### Improvements + +- Improved performance of pdf thumbnail generation + +### Fixes + +- Fixed "Create Tag Group" creating regular tags instead +- Fixed content extending out of window bounds when maximized +- Fixed a crash when preferences file is corrupted +- Fixed rare crash when displaying the update notification +- Fixed multiple possible crashes when dropping files +## COMPASS v1.8.8 (28 June 2025) ### Fixes - Fix crashes on commands in context menu when no items are selected. diff --git a/Deployment/install.iss b/Deployment/install.iss index 90a2e90d..bc9e04e7 100644 --- a/Deployment/install.iss +++ b/Deployment/install.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "COMPASS" -#define MyAppVersion "1.8.8" +#define MyAppVersion "1.8.9" #define MyAppPublisher "Paul De Smul" #define MyAppURL "https://www.compassapp.info" #define MyAppExeName "COMPASS.exe" @@ -39,8 +39,6 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "..\src\bin\Publish\win-x64\*"; DestDir: "{app}"; Flags: ignoreversion Source: "..\src\bin\Publish\win-x64\Media\*"; DestDir: "{app}\Media"; Flags: ignoreversion -Source: "..\src\bin\Publish\win-x64\gs\*"; DestDir: "{app}\gs"; Flags: ignoreversion -Source: "..\src\bin\Publish\win-x64\selenium-manager\windows\*"; DestDir : "{app}\selenium-manager\windows"; Flags: ignoreversion ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] diff --git a/Tests/IntegrationTests/Satchels_Test.cs b/Tests/IntegrationTests/Satchels_Test.cs index 73deebb8..85fb77d6 100644 --- a/Tests/IntegrationTests/Satchels_Test.cs +++ b/Tests/IntegrationTests/Satchels_Test.cs @@ -45,8 +45,8 @@ public async Task TestSatchelExportImport() Assert.IsTrue(importViewModel.ContentSelectorVM.HasCodices, "deserialized satchel has no Codices"); Assert.IsTrue(importViewModel.ContentSelectorVM.HasTags, "deserialized satchel has no Tags"); - Assert.AreEqual(testCollection.AllCodices.Count, deserializedCollection.AllCodices.Count); - Assert.AreEqual(testCollection.AllTags.Count, deserializedCollection.AllTags.Count); + Assert.HasCount(testCollection.AllCodices.Count, deserializedCollection.AllCodices); + Assert.HasCount(testCollection.AllTags.Count, deserializedCollection.AllTags); //Complete import to new Collection importViewModel.CollectionName = "Imported_Satchel"; //cannot use a protect __ name because it is an illegal name @@ -55,8 +55,8 @@ public async Task TestSatchelExportImport() importedCollection = MainViewModel.CollectionVM.CurrentCollection; - Assert.AreEqual(deserializedCollection.AllCodices.Count, importedCollection.AllCodices.Count); - Assert.AreEqual(deserializedCollection.AllTags.Count, importedCollection.AllTags.Count); + Assert.HasCount(deserializedCollection.AllCodices.Count, importedCollection.AllCodices); + Assert.HasCount(deserializedCollection.AllTags.Count, importedCollection.AllTags); } finally { diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 1d20dfd9..110f5301 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -25,14 +25,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/UnitTests/Serialization.cs b/Tests/UnitTests/Serialization.cs index 8b6c0d91..233f7fae 100644 --- a/Tests/UnitTests/Serialization.cs +++ b/Tests/UnitTests/Serialization.cs @@ -21,7 +21,7 @@ public void SerializeSatchelInfo() var newSatchelInfo = JsonSerializer.Deserialize(json); Assert.IsNotNull(newSatchelInfo); - Assert.IsTrue(newSatchelInfo.CreationVersion == Reflection.Version); + Assert.AreEqual(Reflection.Version, newSatchelInfo.CreationVersion); Assert.IsTrue(newSatchelInfo.CreationDate < DateTime.Now); } @@ -40,7 +40,7 @@ public void DeserializeSatchelInfoExtraFields() var satchelInfo = JsonSerializer.Deserialize(json); Assert.IsNotNull(satchelInfo); - Assert.IsTrue(satchelInfo.CreationVersion == "1.200.0"); + Assert.AreEqual("1.200.0", satchelInfo.CreationVersion); } [TestMethod] @@ -54,7 +54,7 @@ public void DeserializeIncompleteSatchelInfo() var satchelInfo = JsonSerializer.Deserialize(json); Assert.IsNotNull(satchelInfo); - Assert.IsTrue(satchelInfo.MinTagsVersion == new SatchelInfo().MinTagsVersion); + Assert.AreEqual(new SatchelInfo().MinTagsVersion, satchelInfo.MinTagsVersion); } [TestMethod] diff --git a/src/COMPASS.csproj b/src/COMPASS.csproj index 893df0a9..b706944f 100644 --- a/src/COMPASS.csproj +++ b/src/COMPASS.csproj @@ -14,22 +14,12 @@ - - - - PreserveNewest - - - - PreserveNewest - - PreserveNewest @@ -49,31 +39,31 @@ - + - - - - - - + + + + + + - + - - - - - + + + + + - + diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 7d1b1b9d..527c66b5 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -40,7 +40,7 @@ // app, or any theme specific resource dictionaries) )] -[assembly: AssemblyVersion("1.8.8")] +[assembly: AssemblyVersion("1.8.9")] [assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")] diff --git a/src/Services/PreferencesService.cs b/src/Services/PreferencesService.cs index 4a6a0038..348b5a7b 100644 --- a/src/Services/PreferencesService.cs +++ b/src/Services/PreferencesService.cs @@ -36,17 +36,28 @@ public void SavePreferences() string tempFileName = PreferencesFilePath + ".tmp"; + lock (writeLocker) { + //cleanup any previous temp file + File.Delete(tempFileName); + + //Write to the temp file using (var writer = XmlWriter.Create(tempFileName, XmlService.XmlWriteSettings)) { XmlSerializer serializer = new(typeof(PreferencesDto)); serializer.Serialize(writer, dto); } + // Verify the temp file was written successfully and has content + if (!File.Exists(tempFileName) || new FileInfo(tempFileName).Length <= 0) + { + Logger.Error($"Failed to write preferences to {tempFileName}", new Exception()); + return; + } + //if successfully written to the tmp file, move to actual path File.Move(tempFileName, PreferencesFilePath, true); - File.Delete(tempFileName); } } catch (UnauthorizedAccessException ex) @@ -65,7 +76,13 @@ public void SavePreferences() public Preferences? LoadPreferences() { - if (File.Exists(PreferencesFilePath)) + if (!File.Exists(PreferencesFilePath)) + { + Logger.Warn($"{PreferencesFilePath} does not exist.", new FileNotFoundException()); + return null; + } + + try { //Label of codexProperties should still be deserialized for backwards compatibility var overrides = new XmlAttributeOverrides(); @@ -76,20 +93,25 @@ public void SavePreferences() XmlSerializer serializer = new(typeof(PreferencesDto), overrides); if (serializer.Deserialize(reader) is PreferencesDto prefsDto) { - return prefsDto is null ? new() : prefsDto.ToModel(); - } - else - { - Logger.Error($"{PreferencesFilePath} could not be read.", new Exception()); - return null; + return prefsDto.ToModel(); } + + Logger.Error($"{PreferencesFilePath} could not be read.", new Exception()); } - else + catch (XmlException ex) { - Logger.Warn($"{PreferencesFilePath} does not exist.", new FileNotFoundException()); - return null; + Logger.Error($"XML parsing error in {PreferencesFilePath}. File may be corrupted or empty.", ex); + } + catch (InvalidOperationException ex) when (ex.InnerException is XmlException) + { + Logger.Error($"XML deserialization error in {PreferencesFilePath}. File may be corrupted or empty.", ex); + } + catch (Exception ex) + { + Logger.Error($"Unexpected error loading preferences from {PreferencesFilePath}", ex); } - } + return null; + } } } diff --git a/src/ViewModels/CodexEditViewModel.cs b/src/ViewModels/CodexEditViewModel.cs index 1ca451dc..30faeb2a 100644 --- a/src/ViewModels/CodexEditViewModel.cs +++ b/src/ViewModels/CodexEditViewModel.cs @@ -13,6 +13,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; @@ -254,28 +255,51 @@ public void OKBtn() public void DragOver(IDropInfo dropInfo) { - if (dropInfo.Data is DataObject data - && data.GetFileDropList().Count == 1 - && IOService.IsImageFile(data.GetFileDropList().Cast().First())) + try { - dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight; - dropInfo.Effects = DragDropEffects.Copy; + if (dropInfo.Data is DataObject data && + data.GetFileDropList() is { Count: 1 } fileDropList && + IOService.IsImageFile(fileDropList.Cast().First())) + { + dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight; + dropInfo.Effects = DragDropEffects.Copy; + return; + } } - else + catch (COMException ex) { - dropInfo.Effects = DragDropEffects.None; + Logger.Error($"COM error accessing drag-drop data during DragOver", ex); } + catch (Exception ex) + { + Logger.Error($"Unexpected error accessing drag-drop data during DragOver", ex); + } + + dropInfo.Effects = DragDropEffects.None; } public void Drop(IDropInfo dropInfo) { - if (dropInfo.Data is DataObject data - && data.GetFileDropList().Count == 1 - && IOService.IsImageFile(data.GetFileDropList().Cast().First())) + try { - string path = data.GetFileDropList().Cast().First(); - CoverService.GetCoverFromImage(path, TempCodex); - RefreshCover(); + if (dropInfo.Data is DataObject data && + data.GetFileDropList() is { Count: 1 } fileDropList && + fileDropList.Cast().Single() is string imgPath && + IOService.IsImageFile(imgPath)) + { + { + CoverService.GetCoverFromImage(imgPath, TempCodex); + RefreshCover(); + } + } + } + catch (COMException ex) + { + Logger.Error($"COM error accessing drag-drop data during Drop", ex); + } + catch (Exception ex) + { + Logger.Error($"Unexpected error during drag-drop operation", ex); } } #endregion diff --git a/src/ViewModels/Layouts/LayoutViewModel.cs b/src/ViewModels/Layouts/LayoutViewModel.cs index 717dbaa1..307beefb 100644 --- a/src/ViewModels/Layouts/LayoutViewModel.cs +++ b/src/ViewModels/Layouts/LayoutViewModel.cs @@ -1,10 +1,12 @@ using COMPASS.Models; using COMPASS.Services; +using COMPASS.Tools; using COMPASS.ViewModels.Import; using GongSolutions.Wpf.DragDrop; using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Windows; namespace COMPASS.ViewModels.Layouts @@ -72,35 +74,52 @@ public async void Drop(IDropInfo dropInfo) { if (dropInfo.Data is DataObject data) { - var paths = data.GetFileDropList(); + try + { + var paths = data.GetFileDropList(); - var folders = paths.Cast().Where(path => File.GetAttributes(path).HasFlag(FileAttributes.Directory)).ToList(); - var files = paths.Cast().Where(path => !File.GetAttributes(path).HasFlag(FileAttributes.Directory)).ToList(); + if (paths == null || paths.Count == 0) + { + Logger.Warn("No file paths found in drag-drop data"); + return; + } - //check for folder import - if (folders.Any()) - { - ImportFolderViewModel folderImportVM = new(manuallyTriggered: true) + var folders = paths.Cast().Where(path => File.GetAttributes(path).HasFlag(FileAttributes.Directory)).ToList(); + var files = paths.Cast().Where(path => !File.GetAttributes(path).HasFlag(FileAttributes.Directory)).ToList(); + + //check for folder import + if (folders.Any()) { - RecursiveDirectories = folders, - Files = files - }; - await folderImportVM.Import(); - } - //If no files or folders, to nothing - else if (!files.Any()) - { - return; + ImportFolderViewModel folderImportVM = new(manuallyTriggered: true) + { + RecursiveDirectories = folders, + Files = files + }; + await folderImportVM.Import(); + } + //If no files or folders, to nothing + else if (!files.Any()) + { + return; + } + //Check if its a cmpss file, do import if so + else if (files.Count == 1 && files.First().EndsWith(Constants.SatchelExtension)) + { + await MainViewModel.CollectionVM.ImportSatchelAsync(files.First()); + } + //If none of the above, just import the files + else + { + await ImportViewModel.ImportFilesAsync(files); + } } - //Check if its a cmpss file, do import if so - else if (files.Count == 1 && files.First().EndsWith(Constants.SatchelExtension)) + catch (COMException ex) { - await MainViewModel.CollectionVM.ImportSatchelAsync(files.First()); + Logger.Error($"COM error accessing drag-drop data during Drop", ex); } - //If none of the above, just import the files - else + catch (Exception ex) { - await ImportViewModel.ImportFilesAsync(files); + Logger.Error($"Unexpected error during drag-drop operation", ex); } } } diff --git a/src/ViewModels/MainViewModel.cs b/src/ViewModels/MainViewModel.cs index 1bf07891..c3ffaf35 100644 --- a/src/ViewModels/MainViewModel.cs +++ b/src/ViewModels/MainViewModel.cs @@ -37,8 +37,6 @@ public MainViewModel() //Start timer that periodically checks if there is an internet connection InitConnectionTimer(); - - MagickNET.SetGhostscriptDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "gs")); } #region Init Functions @@ -70,7 +68,15 @@ private void InitAutoUpdates() string? runningExePath = Process.GetCurrentProcess().MainModule?.FileName; if (!String.IsNullOrWhiteSpace(runningExePath)) { - AutoUpdater.Icon = System.Drawing.Icon.ExtractAssociatedIcon(runningExePath)?.ToBitmap(); + try + { + AutoUpdater.Icon = System.Drawing.Icon.ExtractAssociatedIcon(runningExePath)?.ToBitmap(); + } + catch (Exception ex) + { + // This has failed once because of missing dlls, not that important anyway so just log an error + Logger.Error("Could not extract application icon for auto-updater", ex); + } } #if DEBUG //AutoUpdater.InstalledVersion = new("0.2.0"); //for testing only diff --git a/src/ViewModels/Sources/PdfSourceViewModel.cs b/src/ViewModels/Sources/PdfSourceViewModel.cs index 47d09f5f..1e26342e 100644 --- a/src/ViewModels/Sources/PdfSourceViewModel.cs +++ b/src/ViewModels/Sources/PdfSourceViewModel.cs @@ -78,37 +78,37 @@ public override async Task GetMetaData(SourceSet sources) return codex; } - public override async Task FetchCover(Codex codex) + public override Task FetchCover(Codex codex) { //return false if file doesn't exist if (!IOService.IsPDFFile(codex.Sources.Path) || !File.Exists(codex.Sources.Path)) { - return false; + return Task.FromResult(false); } - try //image.Read can throw exception if file can not be opened/read + if (String.IsNullOrEmpty(codex.CoverArt)) { - using (MagickImage image = new()) - { - await image.ReadAsync(codex.Sources.Path, ReadSettings); - image.Format = MagickFormat.Png; - - //some pdf's are transparent, expecting a white page underneath - image.BackgroundColor = new MagickColor("#FFFFFF"); - image.Alpha(AlphaOption.Remove); + Logger.Error("Trying to write cover img to empty path", new InvalidOperationException()); + return Task.FromResult(false); + } - await CoverService.SaveCover(codex, image); + try //reading an image can throw exception if file can not be opened/read + { + using (var pdfStream = new FileStream(codex.Sources.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + IOService.EnsureFoldersExists(codex.CoverArt); + PDFtoImage.Conversion.SavePng(codex.CoverArt, pdfStream, options: ReadOptions); } - codex.RefreshThumbnail(); - return true; + CoverService.CreateThumbnail(codex); + return Task.FromResult(true); } catch (Exception ex) { Logger.Error($"Failed to generate cover from {Path.GetFileName(codex.Sources.Path)}", ex); LogEntry logEntry = new(Severity.Warning, $"Failed to generate cover from {codex.Title}"); ProgressVM.AddLogEntry(logEntry); - return false; + return Task.FromResult(false); } } @@ -127,5 +127,12 @@ public override async Task FetchCover(Codex codex) FrameCount = 1, // Number of pages Defines = PDFReadDefines, }; + + private static PDFtoImage.RenderOptions? _readOptions; + private static PDFtoImage.RenderOptions ReadOptions => _readOptions ??= + new PDFtoImage.RenderOptions( + BackgroundColor: SkiaSharp.SKColor.Parse("#FFFFFF"), + Width: 850, + WithAspectRatio: true); } } diff --git a/src/ViewModels/TagEditViewModel.cs b/src/ViewModels/TagEditViewModel.cs index 28840263..15a9bc25 100644 --- a/src/ViewModels/TagEditViewModel.cs +++ b/src/ViewModels/TagEditViewModel.cs @@ -89,6 +89,7 @@ public void OKBtn() //reset fields TempTag = new(MainViewModel.CollectionVM.CurrentCollection.AllTags); + TempTag.IsGroup = _editedTag.IsGroup; //If it was initialized as group, reinitialize that field _editedTag = new(); CloseAction(); } diff --git a/src/Windows/MainWindow.xaml.cs b/src/Windows/MainWindow.xaml.cs index b853df3c..9f60a5c1 100644 --- a/src/Windows/MainWindow.xaml.cs +++ b/src/Windows/MainWindow.xaml.cs @@ -73,6 +73,7 @@ public static IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam mmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top); mmi.ptMaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left); mmi.ptMaxSize.Y = Math.Abs(rcWorkArea.Bottom - rcWorkArea.Top); + mmi.ptMaxTrackSize = mmi.ptMaxSize; } Marshal.StructureToPtr(mmi, lParam, true); diff --git a/src/Windows/SettingsWindow.xaml b/src/Windows/SettingsWindow.xaml index fad35d1f..10445fdc 100644 --- a/src/Windows/SettingsWindow.xaml +++ b/src/Windows/SettingsWindow.xaml @@ -673,6 +673,13 @@ BlackPearl + + + + CommunityToolkit + + - - - - Ghostscript - - - - - - - CommunityToolkit - - - + - - 1.8.8 - https://github.com/DSPAUL/COMPASS/releases/download/v1.8.8/COMPASS_Setup_1.8.8.exe + 1.8.9 + https://github.com/DSPAUL/COMPASS/releases/download/v1.8.9/COMPASS_Setup_1.8.9.exe false