diff --git a/Source/FileManager/FolderPickerInitialPath.cs b/Source/FileManager/FolderPickerInitialPath.cs
new file mode 100644
index 00000000..6ad8a9fe
--- /dev/null
+++ b/Source/FileManager/FolderPickerInitialPath.cs
@@ -0,0 +1,87 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace FileManager;
+
+///
+/// Normalizes stored paths for OS folder picker APIs that are stricter than (for example WinForms FolderBrowserDialog / shell SHCreateItemFromParsingName).
+///
+public static class FolderPickerInitialPath
+{
+ private const string WinLongPathPrefix = @"\\?\";
+ private const string WinLongUncPrefix = @"\\?\UNC\";
+
+ ///
+ /// Returns a directory path suitable as a folder picker's starting location, or null to let the OS use its default.
+ /// Verifies the directory exists using the same path rules as before returning a shell-oriented string.
+ ///
+ public static string? GetExistingDirectoryOrNull(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return null;
+
+ path = path.Trim();
+ try
+ {
+ LongPath longPath = path;
+ if (!Directory.Exists(longPath))
+ return null;
+
+ var forPicker = ToOsFolderPickerPath(longPath.Path);
+ if (string.IsNullOrWhiteSpace(forPicker))
+ return null;
+
+ if (!Directory.Exists(forPicker))
+ return null;
+
+ try
+ {
+ var full = Path.GetFullPath(forPicker);
+ if (Directory.Exists(full))
+ return full;
+ }
+ catch (ArgumentException)
+ {
+ // Path.GetFullPath rejects some edge cases; fall through
+ }
+ catch (NotSupportedException)
+ {
+ }
+ catch (PathTooLongException)
+ {
+ }
+
+ return forPicker;
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ catch (NotSupportedException)
+ {
+ return null;
+ }
+ catch (PathTooLongException)
+ {
+ return null;
+ }
+ }
+
+ private static string? ToOsFolderPickerPath(string absolutePathFromLongPath)
+ {
+ if (string.IsNullOrWhiteSpace(absolutePathFromLongPath))
+ return null;
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return absolutePathFromLongPath;
+
+ if (absolutePathFromLongPath.StartsWith(WinLongUncPrefix, StringComparison.OrdinalIgnoreCase))
+ return @"\\" + absolutePathFromLongPath.Substring(WinLongUncPrefix.Length);
+
+ if (absolutePathFromLongPath.StartsWith(WinLongPathPrefix, StringComparison.Ordinal))
+ return absolutePathFromLongPath.Substring(WinLongPathPrefix.Length);
+
+ return absolutePathFromLongPath;
+ }
+}
diff --git a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
index 042bf76c..ebf1f342 100644
--- a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
@@ -3,6 +3,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Dinah.Core;
+using FileManager;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
@@ -124,12 +125,34 @@ public partial class DirectoryOrCustomSelectControl : UserControl
public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
+ var window = this.GetParentWindow();
+ if (window is null)
+ return;
+
var options = new FolderPickerOpenOptions
{
AllowMultiple = false
};
- var selectedFolders = await this.GetParentWindow().StorageProvider.OpenFolderPickerAsync(options);
+ var start = FolderPickerInitialPath.GetExistingDirectoryOrNull(tboxCustomDirPath.Text);
+ if (start is not null)
+ {
+ var loc = await window.StorageProvider.TryGetFolderFromPathAsync(start);
+ if (loc is not null)
+ options.SuggestedStartLocation = loc;
+ }
+
+ IReadOnlyList selectedFolders;
+ try
+ {
+ selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
+ }
+ catch
+ {
+ options.SuggestedStartLocation = null;
+ selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
+ }
+
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
}
}
diff --git a/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs
index 147e30bf..a4d46bbb 100644
--- a/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/LocateAudiobooksDialog.axaml.cs
@@ -3,9 +3,11 @@ using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DataLayer;
using Dinah.Core;
+using FileManager;
using LibationFileManager;
using LibationUiBase;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
@@ -53,10 +55,28 @@ public partial class LocateAudiobooksDialog : DialogWindow
{
Title = "Select the folder to search for audiobooks",
AllowMultiple = false,
- SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? "")
};
- var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
+ var start = FolderPickerInitialPath.GetExistingDirectoryOrNull(Configuration.Instance.Books?.Path ?? "");
+ if (start is not null)
+ {
+ var loc = await StorageProvider.TryGetFolderFromPathAsync(start);
+ if (loc is not null)
+ folderPicker.SuggestedStartLocation = loc;
+ }
+
+ IReadOnlyList picked;
+ try
+ {
+ picked = await StorageProvider.OpenFolderPickerAsync(folderPicker);
+ }
+ catch
+ {
+ folderPicker.SuggestedStartLocation = null;
+ picked = await StorageProvider.OpenFolderPickerAsync(folderPicker);
+ }
+
+ var selectedFolder = picked?.SingleOrDefault()?.TryGetLocalPath();
if (selectedFolder is null || !Directory.Exists(selectedFolder))
{
diff --git a/Source/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs b/Source/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs
index 1dfaea90..28098f10 100644
--- a/Source/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs
+++ b/Source/LibationWinForms/Dialogs/DirectoryOrCustomSelectControl.cs
@@ -1,6 +1,8 @@
-using LibationFileManager;
+using FileManager;
+using LibationFileManager;
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs;
@@ -81,9 +83,21 @@ public partial class DirectoryOrCustomSelectControl : UserControl
using var dialog = new FolderBrowserDialog
{
Description = string.IsNullOrWhiteSpace(dirSearchTitle) ? "Search" : $"Search for {dirSearchTitle}",
- SelectedPath = this.customTb.Text
};
- dialog.ShowDialog();
+ var initial = FolderPickerInitialPath.GetExistingDirectoryOrNull(this.customTb.Text);
+ if (initial is not null)
+ dialog.SelectedPath = initial;
+
+ try
+ {
+ dialog.ShowDialog();
+ }
+ catch (Win32Exception)
+ {
+ dialog.SelectedPath = string.Empty;
+ dialog.ShowDialog();
+ }
+
if (!string.IsNullOrWhiteSpace(dialog.SelectedPath))
this.customTb.Text = dialog.SelectedPath;
}
diff --git a/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs b/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs
index 16eb59bb..cef18693 100644
--- a/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs
+++ b/Source/LibationWinForms/Dialogs/LocateAudiobooksDialog.cs
@@ -1,8 +1,10 @@
using DataLayer;
using Dinah.Core;
+using FileManager;
using LibationFileManager;
using LibationUiBase;
using System;
+using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Windows.Forms;
@@ -44,10 +46,21 @@ public partial class LocateAudiobooksDialog : Form
{
Description = "Select the folder to search for audiobooks",
UseDescriptionForTitle = true,
- InitialDirectory = Configuration.Instance.Books?.Path ?? string.Empty
};
+ var initial = FolderPickerInitialPath.GetExistingDirectoryOrNull(Configuration.Instance.Books?.Path ?? "");
+ if (initial is not null)
+ fbd.InitialDirectory = initial;
- var result = fbd.ShowDialog(this);
+ DialogResult result;
+ try
+ {
+ result = fbd.ShowDialog(this);
+ }
+ catch (Win32Exception)
+ {
+ fbd.InitialDirectory = string.Empty;
+ result = fbd.ShowDialog(this);
+ }
if (result != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
{
DialogResult = result;
diff --git a/Source/_Tests/FileManager.Tests/FolderPickerInitialPathTests.cs b/Source/_Tests/FileManager.Tests/FolderPickerInitialPathTests.cs
new file mode 100644
index 00000000..d156fd5f
--- /dev/null
+++ b/Source/_Tests/FileManager.Tests/FolderPickerInitialPathTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using FileManager;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace FileManager.Tests;
+
+[TestClass]
+public class FolderPickerInitialPathTests
+{
+ [TestMethod]
+ public void GetExistingDirectoryOrNull_NullOrEmpty_ReturnsNull()
+ {
+ Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(null));
+ Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(""));
+ Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(" "));
+ }
+
+ [TestMethod]
+ public void GetExistingDirectoryOrNull_MissingPath_ReturnsNull()
+ {
+ Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "nope")));
+ }
+
+ [TestMethod]
+ public void GetExistingDirectoryOrNull_ExistingTempDir_ReturnsFullPath()
+ {
+ var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "LibationTest_" + Guid.NewGuid().ToString("N"))).FullName;
+ try
+ {
+ var result = FolderPickerInitialPath.GetExistingDirectoryOrNull(dir);
+ Assert.IsNotNull(result);
+ Assert.IsTrue(Directory.Exists(result));
+ }
+ finally
+ {
+ try { Directory.Delete(dir, recursive: true); } catch { /* ignore */ }
+ }
+ }
+}