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