diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 9a0b385d..57b86ec8 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -90,6 +90,7 @@ namespace AppScaffolding Migrations.migrate_to_v6_6_9(config); Migrations.migrate_to_v11_5_0(config); Migrations.migrate_to_v11_6_5(config); + Migrations.migrate_to_v12_0_1(config); } /// Initialize logging. Wire-up events. Run after migration @@ -417,6 +418,82 @@ namespace AppScaffolding public List Filters { get; set; } = new(); } + + public static void migrate_to_v12_0_1(Configuration config) + { +#nullable enable + //Migrate from version 1 file cache to the dictionary-based version 2 cache + const string FILENAME_V1 = "FileLocations.json"; + const string FILENAME_V2 = "FileLocationsV2.json"; + + var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1); + var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2); + + if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1)) + { + try + { + //FilePathCache loads the cache in its static constructor, + //so perform migration without using FilePathCache.CacheEntry + if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0) + return; + + Dictionary cache = new(); + + //Convert to c# objects to speed up searching by ID inside the iterator + var allItems + = v1Cache + .Select(i => new + { + Id = i["Id"]?.Value(), + Path = i["Path"]?["Path"]?.Value() + }).Where(i => i.Id != null) + .ToArray(); + + foreach (var id in allItems.Select(i => i.Id).OfType().Distinct()) + { + //Use this opportunity to purge non-existent files and re-classify file types + //(due to *.aax files previously not being classified as FileType.AAXC) + var items = allItems + .Where(i => i.Id == id && File.Exists(i.Path)) + .Select(i => new JObject + { + { "Id", i.Id }, + { "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) }, + { "Path", new JObject{ { "Path", i.Path } } } + }) + .ToArray(); + + if (items.Length == 0) + continue; + + cache[id] = new JArray(items); + } + + var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } }; + var cacheFileText = cacheJson.ToString(Formatting.Indented); + + void migrate() + { + File.WriteAllText(jsonFileV2, cacheFileText); + File.Delete(jsonFileV1); + } + + try { migrate(); } + catch (IOException) + { + try { migrate(); } + catch (IOException) + { + migrate(); + } + } + } + catch { /* eat */ } + } +#nullable restore + } + public static void migrate_to_v11_6_5(Configuration config) { //Settings migration for unsupported sample rates (#1116) diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index c1f84769..e2373e53 100644 --- a/Source/ApplicationServices/LibraryExporter.cs +++ b/Source/ApplicationServices/LibraryExporter.cs @@ -144,7 +144,7 @@ namespace ApplicationServices PictureId = a.Book.PictureId, IsAbridged = a.Book.IsAbridged, DatePublished = a.Book.DatePublished, - CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "", + CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()), MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating, MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating, MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, diff --git a/Source/LibationAvalonia/App.axaml b/Source/LibationAvalonia/App.axaml index 5ba253c0..e61e851a 100644 --- a/Source/LibationAvalonia/App.axaml +++ b/Source/LibationAvalonia/App.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:LibationAvalonia" xmlns:controls="using:LibationAvalonia.Controls" + xmlns:dialogs="using:LibationAvalonia.Dialogs" x:Class="LibationAvalonia.App" Name="Libation"> @@ -12,6 +13,10 @@ + + + + @@ -81,6 +86,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index 70b091d6..55e2a137 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -44,7 +44,7 @@ namespace LibationAvalonia if (!config.LibationSettingsAreValid) { - var defaultLibationFilesDir = Configuration.UserProfile; + var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory; // check for existing settings in default location var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); @@ -82,8 +82,8 @@ namespace LibationAvalonia // - error message, Exit() if (setupDialog.IsNewUser) { - Configuration.SetLibationFiles(Configuration.UserProfile); - setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books)); + Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory); + setupDialog.Config.Books = Configuration.DefaultBooksDirectory; if (setupDialog.Config.LibationSettingsAreValid) { @@ -174,7 +174,7 @@ namespace LibationAvalonia if (continueResult == DialogResult.Yes) { - config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books)); + config.Books = Configuration.DefaultBooksDirectory; if (config.LibationSettingsAreValid) { diff --git a/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml b/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml index 9c9d7ec2..095307ae 100644 --- a/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml +++ b/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml @@ -91,6 +91,32 @@ S 192,128 147,147 + + M262,8 + h-117 + a 192,200 0 0 0 -36,82 + a 222,334 41 0 0 138,236 + v158 + h-81 + a 16,16 0 0 0 0,32 + h192 + a 16 16 0 0 0 0,-32 + h-81 + v-158 + a 222,334 -41 0 0 138,-236 + a 192,200 0 0 0 -36,-82 + h-117 + m-99,30 + a 192,200 0 0 0 -26,95 + a 187.5,334 35 0 0 125,159 + a 187.5,334 -35 0 0 125,-159 + a 192,200 0 0 0 -26,-95 + h-198 + M158,136 + a 168,305 35 0 0 104,136 + a 168,305 -35 0 0 104,-136 + + diff --git a/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml.cs b/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml.cs index 1f8b02ab..65933290 100644 --- a/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml.cs @@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls { Configuration.KnownDirectories.WinTemp, Configuration.KnownDirectories.UserProfile, + Configuration.KnownDirectories.ApplicationData, Configuration.KnownDirectories.AppDir, + Configuration.KnownDirectories.MyMusic, Configuration.KnownDirectories.MyDocs, Configuration.KnownDirectories.LibationFiles }; diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml b/Source/LibationAvalonia/Controls/Settings/Important.axaml index 9f77b898..6653b82a 100644 --- a/Source/LibationAvalonia/Controls/Settings/Important.axaml +++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml @@ -166,16 +166,7 @@ MinWidth="80" SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}" ItemsSource="{CompiledBinding Themes}"/> - - - + diff --git a/Source/LibationAvalonia/Dialogs/DialogWindow.cs b/Source/LibationAvalonia/Dialogs/DialogWindow.cs index e6213841..38dcd109 100644 --- a/Source/LibationAvalonia/Dialogs/DialogWindow.cs +++ b/Source/LibationAvalonia/Dialogs/DialogWindow.cs @@ -1,4 +1,6 @@ -using Avalonia.Controls; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; using LibationFileManager; using System; using System.Threading.Tasks; @@ -9,19 +11,98 @@ namespace LibationAvalonia.Dialogs { public bool SaveAndRestorePosition { get; set; } = true; public Control ControlToFocusOnShow { get; set; } + protected override Type StyleKeyOverride => typeof(DialogWindow); + + public static readonly StyledProperty UseCustomTitleBarProperty = + AvaloniaProperty.Register(nameof(UseCustomTitleBar)); + + public bool UseCustomTitleBar + { + get { return GetValue(UseCustomTitleBarProperty); } + set { SetValue(UseCustomTitleBarProperty, value); } + } + public DialogWindow() { - this.HideMinMaxBtns(); - this.KeyDown += DialogWindow_KeyDown; - this.Initialized += DialogWindow_Initialized; - this.Opened += DialogWindow_Opened; - this.Closing += DialogWindow_Closing; + KeyDown += DialogWindow_KeyDown; + Initialized += DialogWindow_Initialized; + Opened += DialogWindow_Opened; + Closing += DialogWindow_Closing; + + UseCustomTitleBar = Configuration.IsWindows; } + + private bool fixedMinHeight = false; + private bool fixedMaxHeight = false; + private bool fixedHeight = false; + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + const int customTitleBarHeight = 30; + if (UseCustomTitleBar) + { + if (change.Property == MinHeightProperty && !fixedMinHeight) + { + fixedMinHeight = true; + MinHeight += customTitleBarHeight; + fixedMinHeight = false; + } + if (change.Property == MaxHeightProperty && !fixedMaxHeight) + { + fixedMaxHeight = true; + MaxHeight += customTitleBarHeight; + fixedMaxHeight = false; + } + if (change.Property == HeightProperty && !fixedHeight) + { + fixedHeight = true; + Height += customTitleBarHeight; + fixedHeight = false; + } + } + base.OnPropertyChanged(change); + } + public DialogWindow(bool saveAndRestorePosition) : this() { SaveAndRestorePosition = saveAndRestorePosition; } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (!UseCustomTitleBar) + return; + + var closeButton = e.NameScope.Find