From bfee579719679cf191c8f496e1e1f4d68a80f134 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 6 Nov 2025 13:47:51 -0700 Subject: [PATCH 1/6] Fix DirectoryOrCustomSelectControl --- .../DirectoryOrCustomSelectControl.axaml | 78 +++++-- .../DirectoryOrCustomSelectControl.axaml.cs | 198 +++++++++++------- .../Settings/ImportantSettingsVM.cs | 7 +- .../Configuration.KnownDirectories.cs | 4 +- 4 files changed, 182 insertions(+), 105 deletions(-) diff --git a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml index 761f205d..72cae907 100644 --- a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml +++ b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml @@ -6,29 +6,63 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl"> - - + - + - + - - - + + diff --git a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs index fe51170e..13981d56 100644 --- a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs @@ -1,142 +1,184 @@ using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Platform.Storage; using Dinah.Core; using LibationFileManager; using ReactiveUI; using System.Collections.Generic; +using System.IO; using System.Linq; +#nullable enable namespace LibationAvalonia.Controls { public partial class DirectoryOrCustomSelectControl : UserControl { - public static readonly StyledProperty> KnownDirectoriesProperty = - AvaloniaProperty.Register>(nameof(KnownDirectories), DirectorySelectControl.DefaultKnownDirectories); + public static readonly StyledProperty?> KnownDirectoriesProperty = + AvaloniaProperty.Register?>(nameof(KnownDirectories), DefaultKnownDirectories); - public static readonly StyledProperty SubDirectoryProperty = - AvaloniaProperty.Register(nameof(SubDirectory)); + public static readonly StyledProperty SubDirectoryProperty = + AvaloniaProperty.Register(nameof(SubDirectory)); - public static readonly StyledProperty DirectoryProperty = - AvaloniaProperty.Register(nameof(Directory)); + public static readonly StyledProperty DirectoryProperty = + AvaloniaProperty.Register(nameof(Directory)); - public List KnownDirectories + public IList? KnownDirectories { get => GetValue(KnownDirectoriesProperty); set => SetValue(KnownDirectoriesProperty, value); } - public string Directory + public string? Directory { get => GetValue(DirectoryProperty); set => SetValue(DirectoryProperty, value); } - public string SubDirectory + public string? SubDirectory { get => GetValue(SubDirectoryProperty); set => SetValue(SubDirectoryProperty, value); } - private readonly DirectoryState directoryState = new(); + public static IList DefaultKnownDirectories => [ + Configuration.KnownDirectories.WinTemp, + Configuration.KnownDirectories.UserProfile, + Configuration.KnownDirectories.ApplicationData, + Configuration.KnownDirectories.AppDir, + Configuration.KnownDirectories.MyMusic, + Configuration.KnownDirectories.MyDocs, + Configuration.KnownDirectories.LibationFiles]; + private readonly AvaloniaList _knownDirNames; public DirectoryOrCustomSelectControl() { InitializeComponent(); - - grid.DataContext = directoryState; - - directoryState.PropertyChanged += DirectoryState_PropertyChanged; - PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged; + _knownDirNames = new(GetKnownDirectories(DefaultKnownDirectories)); + cmbKnownDirs.ItemsSource = _knownDirNames; + cmbKnownDirs.SelectionChanged += CmbKnownDirs_SelectionChanged; + btnBrowse.Click += Browse_Click; } - private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private void CmbKnownDirs_SelectionChanged(object? sender, SelectionChangedEventArgs e) { - if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) && - directoryState.KnownChecked && - directoryState.SelectedDirectory is Configuration.KnownDirectories kdir && - kdir is not Configuration.KnownDirectories.None) + if (cmbKnownDirs.SelectedItem is KnownDirectoryItem item && item.Directory is not null) { - Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir); - } - else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) && - directoryState.CustomChecked && - directoryState.CustomDir is not null) - { - Directory = directoryState.CustomDir; + Directory = item.Directory; } } - private class DirectoryState : ViewModels.ViewModelBase - { - private string _customDir; - private string _subDirectory; - private bool _knownChecked; - private bool _customChecked; - private Configuration.KnownDirectories? _selectedDirectory; - public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); } - public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); } - public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); } - public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); } + private IEnumerable GetKnownDirectories(IEnumerable knownDirs) + => knownDirs.Select(k => new KnownDirectoryItem(k, SubDirectory)).Where(k => k.Directory is not null); - public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == SubDirectoryProperty) + { + foreach (var item in _knownDirNames) + { + item.SubDirectory = SubDirectory; + } + VerifyAndApplyDirectory(Directory); + } + else if (change.Property == KnownDirectoriesProperty) + { + var knownDirs = KnownDirectories?.Count > 0 ? KnownDirectories : DefaultKnownDirectories; + if (!_knownDirNames.Select(k => k.KnownDirectory).SequenceEqual(knownDirs)) + { + _knownDirNames.Clear(); + _knownDirNames.AddRange(GetKnownDirectories(knownDirs)); + } + VerifyAndApplyDirectory(Directory); + } + else if (change.Property == DirectoryProperty) + { + VerifyAndApplyDirectory(Directory); + } + + base.OnPropertyChanged(change); } - private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void VerifyAndApplyDirectory(string? directory) { - var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions + if (string.IsNullOrWhiteSpace(Directory)) + return; + + bool dirIsKnown = false; + foreach (var item in _knownDirNames) + { + if (item.IsSamePathAs(directory)) + { + rbKnown.IsChecked = true; + Directory = item.Directory; + cmbKnownDirs.SelectedItem = item; + dirIsKnown = true; + break; + } + } + if (!dirIsKnown) + { + tboxCustomDirPath.Text = directory; + rbCustom.IsChecked = true; + } + } + + public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (VisualRoot is not Window window) + return; + + var options = new FolderPickerOpenOptions { AllowMultiple = false }; - var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options); - - directoryState.CustomDir = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? directoryState.CustomDir; + var selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options); + Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory; } - private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private class KnownDirectoryItem : ReactiveObject { - if (e.Property == DirectoryProperty) + public Configuration.KnownDirectories KnownDirectory { get; set; } + private string? _directory; + public string? Directory { get => _directory; private set => this.RaiseAndSetIfChanged(ref _directory, value); } + public string? Name { get; } + private string? _subDir; + public string? SubDirectory { - var directory = Directory?.Trim() ?? ""; - - var noSubDir = RemoveSubDirectoryFromPath(directory); - var known = Configuration.GetKnownDirectory(noSubDir); - - if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute) - known = Configuration.KnownDirectories.AppDir; - - if (known is Configuration.KnownDirectories.None) + get => _subDir; + set { - directoryState.CustomDir = directory; - directoryState.CustomChecked = true; - } - else - { - directoryState.SelectedDirectory = known; - directoryState.KnownChecked = true; + _subDir = value; + if (Configuration.GetKnownDirectoryPath(KnownDirectory) is string dir) + { + Directory = Path.Combine(dir, _subDir ?? ""); + } } } - else if (e.Property == KnownDirectoriesProperty && - KnownDirectories.Count > 0 && - directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None) - directoryState.SelectedDirectory = KnownDirectories[0]; - } - private string RemoveSubDirectoryFromPath(string path) - { - if (string.IsNullOrWhiteSpace(SubDirectory)) - return path; + public KnownDirectoryItem(Configuration.KnownDirectories known, string? subDir) + { + Name = known.GetDescription(); + KnownDirectory = known; + SubDirectory = subDir; + } - path = path?.Trim() ?? ""; - if (string.IsNullOrWhiteSpace(path)) - return path; + public bool IsSamePathAs(string? otherPath) + { + if (string.IsNullOrWhiteSpace(otherPath) || string.IsNullOrWhiteSpace(Directory)) + return false; - var bottomDir = System.IO.Path.GetFileName(path); - if (SubDirectory.EqualsInsensitive(bottomDir)) - return System.IO.Path.GetDirectoryName(path); + try + { + var p1 = Path.GetFullPath(Directory); + var p2 = Path.GetFullPath(otherPath); + return p1.Equals(p2, System.StringComparison.OrdinalIgnoreCase); + } + catch { return false; } + } - return path; + public override string? ToString() => Name?.ToString(); } } } diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs index 7e88c783..22bf08ee 100644 --- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs +++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs @@ -67,10 +67,11 @@ namespace LibationAvalonia.ViewModels.Settings public List KnownDirectories { get; } = new() { - Configuration.KnownDirectories.UserProfile, - Configuration.KnownDirectories.AppDir, - Configuration.KnownDirectories.MyDocs, + Configuration.KnownDirectories.LibationFiles, Configuration.KnownDirectories.MyMusic, + Configuration.KnownDirectories.MyDocs, + Configuration.KnownDirectories.AppDir, + Configuration.KnownDirectories.UserProfile }; public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books)); diff --git a/Source/LibationFileManager/Configuration.KnownDirectories.cs b/Source/LibationFileManager/Configuration.KnownDirectories.cs index 8159e0ff..e0871f86 100644 --- a/Source/LibationFileManager/Configuration.KnownDirectories.cs +++ b/Source/LibationFileManager/Configuration.KnownDirectories.cs @@ -10,8 +10,8 @@ namespace LibationFileManager { public partial class Configuration { - public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!; - public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}"; + public static string ProcessDirectory { get; } = Path.GetDirectoryName(Environment.ProcessPath)!; + public static string AppDir_Relative => $@".{Path.DirectorySeparatorChar}{LIBATION_FILES_KEY}"; public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY)); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation")); From def0b1f6114483a8b5677fc27ffcfeef3e241154 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 6 Nov 2025 14:46:01 -0700 Subject: [PATCH 2/6] Prevent crash if watched RootDirectory is deleted --- Source/FileManager/BackgroundFileSystem.cs | 14 ++++++-- .../LibationFileManager/AudibleFileStorage.cs | 32 ++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs index 4dec3b8a..0b37a572 100644 --- a/Source/FileManager/BackgroundFileSystem.cs +++ b/Source/FileManager/BackgroundFileSystem.cs @@ -14,7 +14,7 @@ namespace FileManager /// public class BackgroundFileSystem : IDisposable { - public LongPath RootDirectory { get; private set; } + public LongPath? RootDirectory { get; private set; } public string SearchPattern { get; private set; } public SearchOption SearchOption { get; private set; } @@ -51,7 +51,8 @@ namespace FileManager lock (fsCacheLocker) { fsCache.Clear(); - fsCache.AddRange(SafestEnumerateFiles(RootDirectory)); + if (Directory.Exists(RootDirectory)) + fsCache.AddRange(SafestEnumerateFiles(RootDirectory)); } } @@ -60,7 +61,14 @@ namespace FileManager Stop(); lock (fsCacheLocker) - fsCache.AddRange(SafestEnumerateFiles(RootDirectory)); + { + if (!Directory.Exists(RootDirectory)) + { + RootDirectory = null; + return; + } + fsCache.AddRange(SafestEnumerateFiles(RootDirectory)); + } directoryChangesEvents = new BlockingCollection(); fileSystemWatcher = new FileSystemWatcher(RootDirectory) diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs index 0213038c..566469dd 100644 --- a/Source/LibationFileManager/AudibleFileStorage.cs +++ b/Source/LibationFileManager/AudibleFileStorage.cs @@ -146,16 +146,7 @@ namespace LibationFileManager protected override List GetFilePathsCustom(string productId) { - // If user changed the BooksDirectory: reinitialize - lock (bookDirectoryFilesLocker) - { - if (BooksDirectory != BookDirectoryFiles?.RootDirectory) - { - BookDirectoryFiles?.Dispose(); - BookDirectoryFiles = newBookDirectoryFiles(); - } - } - + ValidateBookDirectoryFiles(); var regex = GetBookSearchRegex(productId); var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? []; @@ -172,14 +163,25 @@ namespace LibationFileManager public void Refresh() { - lock (bookDirectoryFilesLocker) - { - BookDirectoryFiles ??= newBookDirectoryFiles(); - } - + ValidateBookDirectoryFiles(); BookDirectoryFiles?.RefreshFiles(); } + private void ValidateBookDirectoryFiles() + { + lock (bookDirectoryFilesLocker) + { + if (BooksDirectory != BookDirectoryFiles?.RootDirectory) + { + //Will happen if the user changed the Books directory + //or if BackgroundFileSystem errored out. + BookDirectoryFiles?.Dispose(); + BookDirectoryFiles = newBookDirectoryFiles(); + } + } + } + + public LongPath? GetPath(string productId) => GetFilePath(productId); public static async IAsyncEnumerable FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken) From bb0dea3fa9c67082ace2e7b37971d5ed870807da Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 6 Nov 2025 21:48:37 -0700 Subject: [PATCH 3/6] Improve EditReplacementChars dialog usability --- .../Controls/DataGridTextColumnExt.cs | 25 +++ .../Dialogs/EditReplacementChars.axaml | 120 ++++++------- .../Dialogs/EditReplacementChars.axaml.cs | 157 ++++++++---------- 3 files changed, 148 insertions(+), 154 deletions(-) create mode 100644 Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs diff --git a/Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs b/Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs new file mode 100644 index 00000000..5768c724 --- /dev/null +++ b/Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace LibationAvalonia.Controls; +internal class DataGridTextColumnExt : DataGridTextColumn +{ + public static readonly StyledProperty MaxLengthProperty = + AvaloniaProperty.Register(nameof(MaxLength)); + + public int MaxLength + { + get => GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs) + { + if (editingElement is TextBox textBox) + { + textBox.MaxLength = MaxLength; + } + return base.PrepareCellForEdit(editingElement, editingEventArgs); + } +} diff --git a/Source/LibationAvalonia/Dialogs/EditReplacementChars.axaml b/Source/LibationAvalonia/Dialogs/EditReplacementChars.axaml index f60f2f16..67e257d8 100644 --- a/Source/LibationAvalonia/Dialogs/EditReplacementChars.axaml +++ b/Source/LibationAvalonia/Dialogs/EditReplacementChars.axaml @@ -2,92 +2,80 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" - MinWidth="500" MinHeight="450" - Width="500" Height="450" + mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="450" + MinWidth="450" MinHeight="450" + Width="450" Height="450" x:Class="LibationAvalonia.Dialogs.EditReplacementChars" xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs" + xmlns:controls="clr-namespace:LibationAvalonia.Controls" x:DataType="dialogs:EditReplacementChars" - Title="Illegal Character Replacement"> - - + x:CompileBindings="True" + Title="File Path Character Replacement"> + - + GridLinesVisibility="All" + CanUserSortColumns="False" + AutoGenerateColumns="False" + ItemsSource="{Binding Replacements}" + KeyDown="replacementGrid_KeyDown" + BeginningEdit="replacementGrid_BeginningEdit" + CellEditEnded="replacementGrid_CellEditEnded" + CellEditEnding="replacementGrid_CellEditEnding"> - - - - - - - - - - - - - - - + - - - - - - - + + + + ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto" + Margin="5"> + + + + - - + + -