Skip to content

Commit

Permalink
feat: update path selection ui and logic
Browse files Browse the repository at this point in the history
Use the executable instead of the folder for selection,
which bypasses issues with old FreeDesktop file portal
pickers not having support for folders. Closes fifty-six#186, fifty-six#177.
  • Loading branch information
fifty-six committed Nov 15, 2023
1 parent 721e175 commit 8298093
Show file tree
Hide file tree
Showing 20 changed files with 367 additions and 111 deletions.
3 changes: 2 additions & 1 deletion Scarab.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Projektanker/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=steamapps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unupdated/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=valvesoftware/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=valvesoftware/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmark/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ public DesignModPageViewModel(ISettings settings,
public IModDatabase Database { get; }
}

public static class MockViewModel
public static class MockPathViewModel
{
public static PathViewModel DesignInstance =>
new PathViewModel(new SuffixNotFoundError("/home/home/Downloads", PathUtil.SUFFIXES));
};

public static class MockModPageViewModel
{
public static DesignModPageViewModel DesignInstance
{
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Resources.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
<value>Dossier Managed ou Assembly-CSharp non trouvé !</value>
</data>
<data name="PU_SelectPath" xml:space="preserve">
<value>Sélectionner le dossier de Hollow Knight.</value>
<value>Sélectionner le {0} de Hollow Knight.</value>
</data>
<data name="PU_InvalidPathTitle" xml:space="preserve">
<value>Chemin</value>
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Resources.hu-HU.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
<value>Hiányzó Managed mappa, vagy Assembly-CSharp!</value>
</data>
<data name="PU_SelectPath" xml:space="preserve">
<value>Add meg a Hollow Knight mappát!</value>
<value>Add meg a Hollow Knight {0}!</value>
</data>
<data name="PU_InvalidPathTitle" xml:space="preserve">
<value>Útvonal</value>
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Resources.pt-BR.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
<value>A pasta Managed ou o Assembly-CSharp está ausente!</value>
</data>
<data name="PU_SelectPath" xml:space="preserve">
<value>Selecione a sua pasta do Hollow Knight.</value>
<value>Selecione a sua {0} do Hollow Knight.</value>
</data>
<data name="PU_InvalidPathTitle" xml:space="preserve">
<value>Caminho</value>
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
<value>Missing Managed folder or Assembly-CSharp!</value>
</data>
<data name="PU_SelectPath" xml:space="preserve">
<value>Select your Hollow Knight folder.</value>
<value>Select your Hollow Knight {0}.</value>
</data>
<data name="PU_InvalidPathTitle" xml:space="preserve">
<value>Path</value>
Expand Down
2 changes: 1 addition & 1 deletion Scarab/Resources.zh.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
<value>缺少Managed或Assembly-CSharp.dll</value>
</data>
<data name="PU_SelectPath" xml:space="preserve">
<value>选择您的空洞骑士文件夹</value>
<value>选择你的空洞骑士 {0}</value>
</data>
<data name="PU_InvalidPathTitle" xml:space="preserve">
<value>路径</value>
Expand Down
21 changes: 13 additions & 8 deletions Scarab/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,25 @@ public static string GetOrCreateDirPath()

internal static bool TryAutoDetect([MaybeNullWhen(false)] out ValidPath path)
{
path = STATIC_PATHS.Select(PathUtil.ValidateWithSuffix).FirstOrDefault(x => x is not null);
var p = STATIC_PATHS.Select(PathUtil.ValidateWithSuffix).FirstOrDefault(x => x is not null);

// If that's valid, use it.
if (path is not null)
if (p is ValidPath v)
{
path = v;
return true;
}

// Otherwise, we go through the user profile suffixes.
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

path = USER_SUFFIX_PATHS
.Select(suffix => Path.Combine(home, suffix))
.Select(PathUtil.ValidateWithSuffix)
.FirstOrDefault(x => x is not null);

return path is not null || TryDetectFromRegistry(out path);
.Select(suffix => Path.Combine(home, suffix))
.Select(PathUtil.ValidateWithSuffix)
.Select(x => x as ValidPath)
.FirstOrDefault(x => x is not null);

return p is not null || TryDetectFromRegistry(out path);
}

private static bool TryDetectFromRegistry([MaybeNullWhen(false)] out ValidPath path)
Expand All @@ -114,7 +118,7 @@ private static bool TryDetectGogRegistry([MaybeNullWhen(false)] out ValidPath pa
return false;

// Double check, just in case.
if (PathUtil.ValidateWithSuffix(gog_path) is not { } validPath)
if (PathUtil.ValidateWithSuffix(gog_path) is not ValidPath validPath)
return false;

path = validPath;
Expand Down Expand Up @@ -164,6 +168,7 @@ or DirectoryNotFoundException

path = library_paths.Select(library_path => Path.Combine(library_path, "steamapps", "common", "Hollow Knight"))
.Select(PathUtil.ValidateWithSuffix)
.Select(x => x as ValidPath)
.FirstOrDefault(x => x is not null);

return path is not null;
Expand Down
145 changes: 60 additions & 85 deletions Scarab/Util/PathUtil.cs
Original file line number Diff line number Diff line change
@@ -1,115 +1,83 @@
using System.Runtime.InteropServices;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
using MessageBox.Avalonia;
using MessageBox.Avalonia.DTO;
using Scarab.Views;

namespace Scarab.Util;

public static class PathUtil
{
// There isn't any [return: MaybeNullWhen(param is null)] so this overload will have to do
// Not really a huge point but it's nice to have the nullable static analysis
public static async Task<string?> SelectPathFallible() => await SelectPath(true);

public static async Task<string> SelectPath(bool fail = false)
public static async Task<string> SelectPath(Window? parent = null)
{
Log.Information("Selecting path...");

Window parent = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow
?? throw new InvalidOperationException();
parent ??= (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow
?? throw new InvalidOperationException();

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return await SelectMacApp(parent, fail);
PathResult res = await TrySelection(parent);

while (true)
{
IStorageFolder? result = (await parent.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
AllowMultiple = false,
Title = Resources.PU_SelectPath
})).FirstOrDefault();


if (result is null)
{
await MessageBoxManager.GetMessageBoxStandardWindow(
Resources.PU_InvalidPathTitle,
Resources.PU_NoSelect
)
.Show();

Log.Information("No path was selected!");
}
else if (ValidateWithSuffix(result.Path.LocalPath) is not var (managed, suffix))
if (res is not ValidPath (var managed, var suffix))
{
await MessageBoxManager.GetMessageBoxStandardWindow(
new MessageBoxStandardParams
{
ContentTitle = Resources.PU_InvalidPathTitle,
ContentHeader = Resources.PU_InvalidPathHeader,
ContentMessage = Resources.PU_InvalidPath,
MinHeight = 140
}
)
.Show();
Log.Information("Invalid path selection! {Result}", res);

Log.Information("User selected invalid path {Path}", result.Path.LocalPath);
var w = new PathWindow { ViewModel = new PathViewModel(res) };

// The dialog asks the user to select again, so we check
// if we got a non-null path back from it
if (await w.ShowDialog<string?>(parent) is { } p)
return p;
}
else
{
return Path.Combine(managed, suffix);
}

if (fail)
return null!;
}
}

// ReSharper disable once SuggestBaseTypeForParameter
private static async Task<string> SelectMacApp(Window parent, bool fail)
public static async Task<PathResult> TrySelection(Window? parent = null)
{
while (true)
string LocalizeToOS()
{
IStorageFile? result = (await parent.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
AllowMultiple = false,
FileTypeFilter = new[] { new FilePickerFileType("app") { Patterns = new[] { "*.app" } } }
}
)).FirstOrDefault();

if (result is null)
{
await MessageBoxManager.GetMessageBoxStandardWindow(
Resources.PU_InvalidPathTitle,
Resources.PU_NoSelectMac
)
.Show();

Log.Information("No path was selected!");
}
// Don't need to log these, as ValidateWithSuffix does so for us
else if (ValidateWithSuffix(result.Path.LocalPath) is not var (managed, suffix))
{
await MessageBoxManager.GetMessageBoxStandardWindow(new MessageBoxStandardParams {
ContentTitle = Resources.PU_InvalidPathTitle,
ContentHeader = Resources.PU_InvalidAppHeader,
ContentMessage = Resources.PU_InvalidApp,
MinHeight = 200
}).Show();
}
else
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return string.Format(Resources.PU_SelectPath, "app");

// ReSharper disable once ConvertIfStatementToReturnStatement
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return string.Format(Resources.PU_SelectPath, "exe");

// Default to the linux one,
return string.Format(Resources.PU_SelectPath, "hollow_knight.x86_64");
}

parent ??= Application.Current?.ApplicationLifetime is
IClassicDesktopStyleApplicationLifetime { MainWindow: { } main }
? main
: throw new InvalidOperationException("No window found!");

IStorageFile? result = (await parent.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
return Path.Combine(managed, suffix);
Title = LocalizeToOS(),
AllowMultiple = false,
FileTypeFilter = new[] { new FilePickerFileType("Hollow Knight file") {
Patterns = new[] { "*.app", "*.exe", "*.x86_64" }
} }
}
)).FirstOrDefault();

if (fail)
return null!;
}
}
if (result is not { Path.LocalPath: var localPath })
return new PathNotSelectedError();

private static readonly string[] SUFFIXES =
var path = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? localPath
: Path.GetDirectoryName(localPath)!;

return ValidateWithSuffix(path);
}

internal static readonly string[] SUFFIXES =
{
// GoG
"Hollow Knight_Data/Managed",
Expand All @@ -119,10 +87,13 @@ await MessageBoxManager.GetMessageBoxStandardWindow(new MessageBoxStandardParams
"Contents/Resources/Data/Managed"
};

public static ValidPath? ValidateWithSuffix(string root)
public static PathResult ValidateWithSuffix(string? root)
{
if (root is null)
return new PathNotSelectedError();

if (!Directory.Exists(root))
return null;
return new RootNotFoundError();

string? suffix = SUFFIXES.FirstOrDefault(s =>
{
Expand All @@ -136,7 +107,8 @@ await MessageBoxManager.GetMessageBoxStandardWindow(new MessageBoxStandardParams
if (suffix is null)
{
Log.Information("Selected path root {Root} had no valid suffix with Managed folder!", root);
return null;

return new SuffixNotFoundError(root, SUFFIXES.Select(s => Path.Combine(root, s)).ToArray());
}

if (File.Exists(Path.Combine(root, suffix, "Assembly-CSharp.dll")))
Expand All @@ -151,7 +123,10 @@ await MessageBoxManager.GetMessageBoxStandardWindow(new MessageBoxStandardParams
suffix
);

return null;
return new AssemblyNotFoundError(
Path.Combine(root, suffix),
new[] { Path.Combine(root, suffix, "Assembly-CSharp.dll") }
);

}

Expand Down
23 changes: 22 additions & 1 deletion Scarab/Util/ValidPath.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
namespace Scarab.Util;

public record ValidPath(string Root, string Suffix);
public abstract record PathResult
{
public string? Path => this switch
{
// Not a failure
ValidPath v => System.IO.Path.Combine(v.Root, v.Suffix),

RootNotFoundError => null,
SuffixNotFoundError s => s.Root,
AssemblyNotFoundError a => a.Root,
PathNotSelectedError => null,

_ => throw new ArgumentOutOfRangeException()
};
}

public record ValidPath(string Root, string Suffix) : PathResult;

public record RootNotFoundError : PathResult;
public record SuffixNotFoundError(string Root, string[] AttemptedSuffixes) : PathResult;
public record AssemblyNotFoundError(string Root, string[] MissingFiles) : PathResult;
public record PathNotSelectedError : PathResult;
26 changes: 26 additions & 0 deletions Scarab/ViewModels/PathViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Scarab.ViewModels;

[UsedImplicitly]
public partial class PathViewModel : ViewModelBase
{
public string? Selection => Result.Path;

[Notify]
private PathResult _result;

public ReactiveCommand<Unit, Unit> ChangePath { get; }

public PathViewModel(PathResult res)
{
ChangePath = ReactiveCommand.CreateFromTask(ChangePathAsync);
Log.Debug("Result = {Result}", res);
Result = _result = res;
}

private async Task ChangePathAsync()
{
Result = await PathUtil.TrySelection();

Log.Information("Set selection to new path: {Path}", Selection);
}
}
Loading

0 comments on commit 8298093

Please sign in to comment.