mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-05-13 10:26:59 -04:00
Merge pull request #1806 from rmcrackan/rmcrackan/1804-win-folder-picker-custom-paths
#1804 - Fix Windows folder picker crash on custom paths
This commit is contained in:
87
Source/FileManager/FolderPickerInitialPath.cs
Normal file
87
Source/FileManager/FolderPickerInitialPath.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FileManager;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes stored paths for OS folder picker APIs that are stricter than <see cref="Directory"/> (for example WinForms <c>FolderBrowserDialog</c> / shell <c>SHCreateItemFromParsingName</c>).
|
||||
/// </summary>
|
||||
public static class FolderPickerInitialPath
|
||||
{
|
||||
private const string WinLongPathPrefix = @"\\?\";
|
||||
private const string WinLongUncPrefix = @"\\?\UNC\";
|
||||
|
||||
/// <summary>
|
||||
/// Returns a directory path suitable as a folder picker's starting location, or <c>null</c> to let the OS use its default.
|
||||
/// Verifies the directory exists using the same path rules as <see cref="LongPath"/> before returning a shell-oriented string.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<IStorageFolder> selectedFolders;
|
||||
try
|
||||
{
|
||||
selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
|
||||
}
|
||||
catch
|
||||
{
|
||||
options.SuggestedStartLocation = null;
|
||||
selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
|
||||
}
|
||||
|
||||
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IStorageFolder> 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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user