From 2edea276d40bcee5e3fec1671cfb64eb53d8ae53 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 01:58:50 +0900 Subject: [PATCH 01/22] Init --- .../Data/Contracts/ICommonDialogService.cs | 10 +++ .../Helpers/Application/AppLifecycleHelper.cs | 1 + src/Files.App/NativeMethods.txt | 6 +- src/Files.App/Services/CommonDialogService.cs | 82 +++++++++++++++++++ .../Properties/CustomizationViewModel.cs | 42 ++++------ 5 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 src/Files.App/Data/Contracts/ICommonDialogService.cs create mode 100644 src/Files.App/Services/CommonDialogService.cs diff --git a/src/Files.App/Data/Contracts/ICommonDialogService.cs b/src/Files.App/Data/Contracts/ICommonDialogService.cs new file mode 100644 index 000000000000..fb877571f321 --- /dev/null +++ b/src/Files.App/Data/Contracts/ICommonDialogService.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + public interface ICommonDialogService + { + string Open_FileOpenDialog(nint hWnd, string[] filters); + } +} diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 67fb24dc9542..12ce7c5fbf96 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -163,6 +163,7 @@ public static IHost ConfigureHost() // Services .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Files.App/NativeMethods.txt b/src/Files.App/NativeMethods.txt index 8b16a3515a08..3507a832c699 100644 --- a/src/Files.App/NativeMethods.txt +++ b/src/Files.App/NativeMethods.txt @@ -45,4 +45,8 @@ MoveFileFromApp DeleteFileFromApp RemoveDirectoryFromApp GetKeyState -CreateDirectoryFromApp \ No newline at end of file +CreateDirectoryFromApp +CoCreateInstance +FileOpenDialog +IFileOpenDialog +SHCreateItemFromParsingName diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs new file mode 100644 index 000000000000..10aef1f209a2 --- /dev/null +++ b/src/Files.App/Services/CommonDialogService.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; + +namespace Files.App.Services +{ + public class CommonDialogService : ICommonDialogService + { + public string Open_FileOpenDialog(nint hWnd, string[] filters) + { + try + { + unsafe + { + // Get a new instance of the OpenFileDialog + PInvoke.CoCreateInstance( + typeof(FileOpenDialog).GUID, + null, + CLSCTX.CLSCTX_INPROC_SERVER, + out IFileOpenDialog openDialog) + .ThrowOnFailure(); + + List extensions = []; + + if (filters.Length is not 0 && filters.Length % 2 is 0) + { + // All even numbered tokens should be labels. + // Odd numbered tokens are the associated extensions. + for (int i = 1; i < filters.Length; i += 2) + { + COMDLG_FILTERSPEC extension; + + extension.pszSpec = (char*)Marshal.StringToHGlobalUni(filters[i]); + extension.pszName = (char*)Marshal.StringToHGlobalUni(filters[i - 1]); + + // Add to the exclusive extension list + extensions.Add(extension); + } + } + + // Set the file type using the extension list + openDialog.SetFileTypes(extensions.ToArray()); + + // Get the default shell folder (My Computer) + PInvoke.SHCreateItemFromParsingName( + Environment.GetFolderPath(Environment.SpecialFolder.MyComputer), + null, + typeof(IShellItem).GUID, + out var directoryShellItem) + .ThrowOnFailure(); + + // Set the default folder to open in the dialog + openDialog.SetFolder((IShellItem)directoryShellItem); + openDialog.SetDefaultFolder((IShellItem)directoryShellItem); + + // Show the dialog + openDialog.Show(new HWND(hWnd)); + + // Get the file that user chose + openDialog.GetResult(out var resultShellItem); + resultShellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var lpFilePath); + var filePath = lpFilePath.ToString(); + + return filePath; + } + } + catch (Exception ex) + { + App.Logger.LogError(ex, "Failed to open a common dialog called OpenFileDialog."); + + return string.Empty; + } + } + } +} diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 4cfdc9c2052e..63e7ccb7c89a 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -3,11 +3,14 @@ using System.IO; using Windows.Storage.Pickers; using Microsoft.UI.Windowing; +using System.Windows.Input; namespace Files.App.ViewModels.Properties { public sealed class CustomizationViewModel : ObservableObject { + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + private static string DefaultIconDllFilePath => Path.Combine(Constants.UserEnvironmentPaths.SystemRootPath, "System32", "SHELL32.dll"); @@ -41,8 +44,8 @@ public IconFileInfo? SelectedDllIcon } } - public IRelayCommand RestoreDefaultIconCommand { get; private set; } - public IAsyncRelayCommand OpenFilePickerCommand { get; private set; } + public ICommand RestoreDefaultIconCommand { get; private set; } + public ICommand OpenFilePickerCommand { get; private set; } public CustomizationViewModel(IShellPage appInstance, BaseProperties baseProperties, AppWindow appWindow) { @@ -66,40 +69,31 @@ public CustomizationViewModel(IShellPage appInstance, BaseProperties basePropert // Get default LoadIconsForPath(IconResourceItemPath); - RestoreDefaultIconCommand = new RelayCommand(ExecuteRestoreDefaultIcon); - OpenFilePickerCommand = new AsyncRelayCommand(ExecuteOpenFilePickerAsync); + RestoreDefaultIconCommand = new RelayCommand(ExecuteRestoreDefaultIconCommand); + OpenFilePickerCommand = new RelayCommand(ExecuteOpenFilePickerCommand); } - private void ExecuteRestoreDefaultIcon() + private void ExecuteRestoreDefaultIconCommand() { SelectedDllIcon = null; _isIconChanged = true; } - private async Task ExecuteOpenFilePickerAsync() + private void ExecuteOpenFilePickerCommand() { - // Initialize picker - FileOpenPicker picker = new() - { - SuggestedStartLocation = PickerLocationId.ComputerFolder, - ViewMode = PickerViewMode.Thumbnail, - }; - - picker.FileTypeFilter.Add(".dll"); - picker.FileTypeFilter.Add(".exe"); - picker.FileTypeFilter.Add(".ico"); - - // WINUI3: Create and initialize new window var parentWindowId = _appWindow.Id; var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); - WinRT.Interop.InitializeWithWindow.Initialize(picker, handle); - // Open picker - var file = await picker.PickSingleFileAsync(); - if (file is null) - return; + var filePath = + CommonDialogService.Open_FileOpenDialog( + handle, + [ + "Application extension", "*.dll", + "Application", "*.exe", + "ICO File", "*.ico", + ]); - LoadIconsForPath(file.Path); + LoadIconsForPath(filePath); } public async Task UpdateIcon() From da8819d3b03449267c6b94cf9030078b5b1649b6 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 02:11:25 +0900 Subject: [PATCH 02/22] Added xml comments --- .../Data/Contracts/ICommonDialogService.cs | 12 ++++++++++++ src/Files.App/Services/CommonDialogService.cs | 2 ++ .../Properties/CustomizationViewModel.cs | 16 ++++++++-------- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Files.App/Data/Contracts/ICommonDialogService.cs b/src/Files.App/Data/Contracts/ICommonDialogService.cs index fb877571f321..1131e39aa6f9 100644 --- a/src/Files.App/Data/Contracts/ICommonDialogService.cs +++ b/src/Files.App/Data/Contracts/ICommonDialogService.cs @@ -3,8 +3,20 @@ namespace Files.App.Data.Contracts { + /// + /// Provides service to launch common dialog through Win32API. + /// public interface ICommonDialogService { + /// + /// Opens a common dialog called FileOpenDialog through native Win32API. + /// + /// The Window handle that the dialog launches based on. + /// The extension filters that the dialog uses to exclude unnecessary files.
The filter must have a pair:[ "Application", ".exe" ] + /// + /// There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. + /// + /// The file path that user chose. string Open_FileOpenDialog(nint hWnd, string[] filters); } } diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index 10aef1f209a2..76b978b88804 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -11,8 +11,10 @@ namespace Files.App.Services { + /// public class CommonDialogService : ICommonDialogService { + /// public string Open_FileOpenDialog(nint hWnd, string[] filters) { try diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 63e7ccb7c89a..317e105abf57 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -84,14 +84,14 @@ private void ExecuteOpenFilePickerCommand() var parentWindowId = _appWindow.Id; var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); - var filePath = - CommonDialogService.Open_FileOpenDialog( - handle, - [ - "Application extension", "*.dll", - "Application", "*.exe", - "ICO File", "*.ico", - ]); + string[] extensions = + [ + "Application extension", "*.dll", + "Application", "*.exe", + "ICO File", "*.ico", + ]; + + var filePath = CommonDialogService.Open_FileOpenDialog(handle, extensions); LoadIconsForPath(filePath); } From cc06697685d5f208af9383df5f825e37419a1c24 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 03:02:34 +0900 Subject: [PATCH 03/22] Added FileSaveDialog & NetworkConnectionDialog --- .../Data/Contracts/ICommonDialogService.cs | 30 ++- .../Data/Models/NetworkConnectionDialog.cs | 118 ---------- src/Files.App/NativeMethods.txt | 2 + src/Files.App/Services/CommonDialogService.cs | 220 ++++++++++++++++-- .../Services/NetworkDrivesService.cs | 15 +- .../Properties/CustomizationViewModel.cs | 4 +- .../ViewModels/Settings/AdvancedViewModel.cs | 8 +- 7 files changed, 246 insertions(+), 151 deletions(-) delete mode 100644 src/Files.App/Data/Models/NetworkConnectionDialog.cs diff --git a/src/Files.App/Data/Contracts/ICommonDialogService.cs b/src/Files.App/Data/Contracts/ICommonDialogService.cs index 1131e39aa6f9..41a5f727e3c6 100644 --- a/src/Files.App/Data/Contracts/ICommonDialogService.cs +++ b/src/Files.App/Data/Contracts/ICommonDialogService.cs @@ -13,10 +13,34 @@ public interface ICommonDialogService /// /// The Window handle that the dialog launches based on. /// The extension filters that the dialog uses to exclude unnecessary files.
The filter must have a pair:[ "Application", ".exe" ] + /// The file that that user chose. /// - /// There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. + /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. /// - /// The file path that user chose. - string Open_FileOpenDialog(nint hWnd, string[] filters); + /// True if the 'Open' button was clicked; otherwise, false. + bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath); + + /// + /// Opens a common dialog called FileSaveDialog through native Win32API. + /// + /// The Window handle that the dialog launches based on. + /// The extension filters that the dialog uses to exclude unnecessary files.
The filter must have a pair:[ "Application", ".exe" ] + /// The file that that user chose. + /// + /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. + /// + /// True if the 'Open' button was clicked; otherwise, false. + bool Open_FileSaveDialog(nint hWnd, string[] filters, out string filePath); + + /// + /// Opens a common dialog called NetworkConnectionDialog through native Win32API. + /// + /// The value indicating whether to hide the check box allowing the user to restore the connection at logon. + /// The value indicating whether restore the connection at logon. + /// The value indicating whether to display a read-only path instead of allowing the user to type in a path. This is only valid if is not . + /// The name of the remote network. + /// The value indicating whether to enter the most recently used paths into the combination box. + /// True if the 'OK' button was clicked; otherwise, false. + bool Open_NetworkConnectionDialog(nint hWind, bool hideRestoreConnectionCheckBox = false, bool persistConnectionAtLogon = false, bool readOnlyPath = false, string? remoteNetworkName = null, bool useMostRecentPath = false); } } diff --git a/src/Files.App/Data/Models/NetworkConnectionDialog.cs b/src/Files.App/Data/Models/NetworkConnectionDialog.cs deleted file mode 100644 index e9d3c35ee553..000000000000 --- a/src/Files.App/Data/Models/NetworkConnectionDialog.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using System.Runtime.InteropServices; -using System.Windows.Forms; -using Vanara.Extensions; -using Vanara.InteropServices; -using static Vanara.PInvoke.Mpr; - -namespace Files.App.Data.Models -{ - /// - /// A dialog box that allows the user to browse and connect to network resources. - /// - /// - /// Forked from Vanara. - /// - public sealed class NetworkConnectionDialog : CommonDialog - { - private readonly NETRESOURCE netRes = new(); - private CONNECTDLGSTRUCT dialogOptions; - - /// Initializes a new instance of the class. - public NetworkConnectionDialog() - { - dialogOptions.cbStructure = (uint)Marshal.SizeOf(typeof(CONNECTDLGSTRUCT)); - netRes.dwType = NETRESOURCEType.RESOURCETYPE_DISK; - } - - /// Gets the connected device number. This value is only valid after successfully running the dialog. - /// The connected device number. The value is 1 for A:, 2 for B:, 3 for C:, and so on. If the user made a deviceless connection, the value is –1. - [Browsable(false)] - public int ConnectedDeviceNumber => dialogOptions.dwDevNum; - - /// Gets or sets a value indicating whether to hide the check box allowing the user to restore the connection at logon. - /// true if hiding restore connection check box; otherwise, false. - [DefaultValue(false), Category("Appearance"), Description("Hide the check box allowing the user to restore the connection at logon.")] - public bool HideRestoreConnectionCheckBox - { - get => dialogOptions.dwFlags.IsFlagSet(CONN_DLG.CONNDLG_HIDE_BOX); - set => dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(CONN_DLG.CONNDLG_HIDE_BOX, value); - } - - /// Gets or sets a value indicating whether restore the connection at logon. - /// true to restore connection at logon; otherwise, false. - [DefaultValue(false), Category("Behavior"), Description("Restore the connection at logon.")] - public bool PersistConnectionAtLogon - { - get => dialogOptions.dwFlags.IsFlagSet(CONN_DLG.CONNDLG_PERSIST); - set - { - dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(CONN_DLG.CONNDLG_PERSIST, value); - dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(CONN_DLG.CONNDLG_NOT_PERSIST, !value); - } - } - - /// - /// Gets or sets a value indicating whether to display a read-only path instead of allowing the user to type in a path. This is only - /// valid if is not . - /// - /// true to display a read only path; otherwise, false. - [DefaultValue(false), Category("Appearance"), Description("Display a read-only path instead of allowing the user to type in a path.")] - public bool ReadOnlyPath { get; set; } - - /// Gets or sets the name of the remote network. - /// The name of the remote network. - [DefaultValue(null), Category("Behavior"), Description("The value displayed in the path field.")] - public string RemoteNetworkName { get => netRes.lpRemoteName; set => netRes.lpRemoteName = value; } - - /// Gets or sets a value indicating whether to enter the most recently used paths into the combination box. - /// true to use MRU path; otherwise, false. - /// UseMostRecentPath - [DefaultValue(false), Category("Behavior"), Description("Enter the most recently used paths into the combination box.")] - public bool UseMostRecentPath - { - get => dialogOptions.dwFlags.IsFlagSet(CONN_DLG.CONNDLG_USE_MRU); - set - { - if (value && !string.IsNullOrEmpty(RemoteNetworkName)) - throw new InvalidOperationException($"{nameof(UseMostRecentPath)} cannot be set to true if {nameof(RemoteNetworkName)} has a value."); - - dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(CONN_DLG.CONNDLG_USE_MRU, value); - } - } - - /// - public override void Reset() - { - dialogOptions.dwDevNum = -1; - dialogOptions.dwFlags = 0; - dialogOptions.lpConnRes = IntPtr.Zero; - ReadOnlyPath = false; - } - - /// - protected override bool RunDialog(IntPtr hwndOwner) - { - using var lpNetResource = SafeCoTaskMemHandle.CreateFromStructure(netRes); - - dialogOptions.hwndOwner = hwndOwner; - dialogOptions.lpConnRes = lpNetResource.DangerousGetHandle(); - - if (ReadOnlyPath && !string.IsNullOrEmpty(netRes.lpRemoteName)) - dialogOptions.dwFlags |= CONN_DLG.CONNDLG_RO_PATH; - - var result = WNetConnectionDialog1(dialogOptions); - - dialogOptions.lpConnRes = IntPtr.Zero; - - if (result == unchecked((uint)-1)) - return false; - - result.ThrowIfFailed(); - - return true; - } - } -} diff --git a/src/Files.App/NativeMethods.txt b/src/Files.App/NativeMethods.txt index 3507a832c699..c151d814c8bb 100644 --- a/src/Files.App/NativeMethods.txt +++ b/src/Files.App/NativeMethods.txt @@ -50,3 +50,5 @@ CoCreateInstance FileOpenDialog IFileOpenDialog SHCreateItemFromParsingName +FileSaveDialog +IFileSaveDialog diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index 76b978b88804..63c970d80d71 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; +using System.Windows.Forms; +using Vanara.Extensions; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; @@ -12,29 +14,29 @@ namespace Files.App.Services { /// - public class CommonDialogService : ICommonDialogService + public sealed class CommonDialogService : ICommonDialogService { /// - public string Open_FileOpenDialog(nint hWnd, string[] filters) + public bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath) { + filePath = string.Empty; + try { unsafe { - // Get a new instance of the OpenFileDialog + // Get a new instance of the dialog PInvoke.CoCreateInstance( typeof(FileOpenDialog).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, - out IFileOpenDialog openDialog) + out IFileOpenDialog dialog) .ThrowOnFailure(); List extensions = []; if (filters.Length is not 0 && filters.Length % 2 is 0) { - // All even numbered tokens should be labels. - // Odd numbered tokens are the associated extensions. for (int i = 1; i < filters.Length; i += 2) { COMDLG_FILTERSPEC extension; @@ -48,7 +50,7 @@ public string Open_FileOpenDialog(nint hWnd, string[] filters) } // Set the file type using the extension list - openDialog.SetFileTypes(extensions.ToArray()); + dialog.SetFileTypes(extensions.ToArray()); // Get the default shell folder (My Computer) PInvoke.SHCreateItemFromParsingName( @@ -59,25 +61,213 @@ public string Open_FileOpenDialog(nint hWnd, string[] filters) .ThrowOnFailure(); // Set the default folder to open in the dialog - openDialog.SetFolder((IShellItem)directoryShellItem); - openDialog.SetDefaultFolder((IShellItem)directoryShellItem); + dialog.SetFolder((IShellItem)directoryShellItem); + dialog.SetDefaultFolder((IShellItem)directoryShellItem); // Show the dialog - openDialog.Show(new HWND(hWnd)); + dialog.Show(new HWND(hWnd)); // Get the file that user chose - openDialog.GetResult(out var resultShellItem); + dialog.GetResult(out var resultShellItem); resultShellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var lpFilePath); - var filePath = lpFilePath.ToString(); + filePath = lpFilePath.ToString(); - return filePath; + return true; } } catch (Exception ex) { - App.Logger.LogError(ex, "Failed to open a common dialog called OpenFileDialog."); + App.Logger.LogError(ex, "Failed to open a common dialog called FileOpenDialog."); + + return false; + } + } + + /// + public bool Open_FileSaveDialog(nint hWnd, string[] filters, out string filePath) + { + filePath = string.Empty; + + try + { + unsafe + { + // Get a new instance of the dialog + PInvoke.CoCreateInstance( + typeof(FileSaveDialog).GUID, + null, + CLSCTX.CLSCTX_INPROC_SERVER, + out IFileSaveDialog dialog) + .ThrowOnFailure(); + + List extensions = []; + + if (filters.Length is not 0 && filters.Length % 2 is 0) + { + for (int i = 1; i < filters.Length; i += 2) + { + COMDLG_FILTERSPEC extension; + + extension.pszSpec = (char*)Marshal.StringToHGlobalUni(filters[i]); + extension.pszName = (char*)Marshal.StringToHGlobalUni(filters[i - 1]); + + // Add to the exclusive extension list + extensions.Add(extension); + } + } + + // Set the file type using the extension list + dialog.SetFileTypes(extensions.ToArray()); + + // Get the default shell folder (My Computer) + PInvoke.SHCreateItemFromParsingName( + Environment.GetFolderPath(Environment.SpecialFolder.MyComputer), + null, + typeof(IShellItem).GUID, + out var directoryShellItem) + .ThrowOnFailure(); + + // Set the default folder to open in the dialog + dialog.SetFolder((IShellItem)directoryShellItem); + dialog.SetDefaultFolder((IShellItem)directoryShellItem); + + // Show the dialog + dialog.Show(new HWND(hWnd)); + + // Get the file that user chose + dialog.GetResult(out var resultShellItem); + resultShellItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out var lpFilePath); + filePath = lpFilePath.ToString(); + + return true; + } + } + catch (Exception ex) + { + App.Logger.LogError(ex, "Failed to open a common dialog called FileSaveDialog."); + + return false; + } + } + + /// + public bool Open_NetworkConnectionDialog(nint hWind, bool hideRestoreConnectionCheckBox = false, bool persistConnectionAtLogon = false, bool readOnlyPath = false, string? remoteNetworkName = null, bool useMostRecentPath = false) + { + using var dialog = new NetworkConnectionDialog() + { + HideRestoreConnectionCheckBox = hideRestoreConnectionCheckBox, + PersistConnectionAtLogon = persistConnectionAtLogon, + ReadOnlyPath = readOnlyPath, + RemoteNetworkName = remoteNetworkName!, + UseMostRecentPath = useMostRecentPath, + }; + + var window = Win32Helper.Win32Window.FromLong(hWind.ToInt64()); + + return dialog.ShowDialog(window) == System.Windows.Forms.DialogResult.OK; + } + + private sealed class NetworkConnectionDialog : CommonDialog + { + private readonly Vanara.PInvoke.Mpr.NETRESOURCE netRes = new(); + private Vanara.PInvoke.Mpr.CONNECTDLGSTRUCT dialogOptions; + + /// + /// Initializes a new instance of the class. + /// + public NetworkConnectionDialog() + { + dialogOptions.cbStructure = (uint)Marshal.SizeOf(typeof(Vanara.PInvoke.Mpr.CONNECTDLGSTRUCT)); + netRes.dwType = Vanara.PInvoke.Mpr.NETRESOURCEType.RESOURCETYPE_DISK; + } + + /// Gets the connected device number. This value is only valid after successfully running the dialog. + /// The connected device number. The value is 1 for A:, 2 for B:, 3 for C:, and so on. If the user made a deviceless connection, the value is –1. + [Browsable(false)] + public int ConnectedDeviceNumber + => dialogOptions.dwDevNum; + + /// Gets or sets a value indicating whether to hide the check box allowing the user to restore the connection at logon. + /// true if hiding restore connection check box; otherwise, false. + [DefaultValue(false), Category("Appearance"), Description("Hide the check box allowing the user to restore the connection at logon.")] + public bool HideRestoreConnectionCheckBox + { + get => dialogOptions.dwFlags.IsFlagSet(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_HIDE_BOX); + set => dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_HIDE_BOX, value); + } + + /// Gets or sets a value indicating whether restore the connection at logon. + /// true to restore connection at logon; otherwise, false. + [DefaultValue(false), Category("Behavior"), Description("Restore the connection at logon.")] + public bool PersistConnectionAtLogon + { + get => dialogOptions.dwFlags.IsFlagSet(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_PERSIST); + set + { + dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_PERSIST, value); + dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_NOT_PERSIST, !value); + } + } + + /// + /// Gets or sets a value indicating whether to display a read-only path instead of allowing the user to type in a path. This is only + /// valid if is not . + /// + /// true to display a read only path; otherwise, false. + [DefaultValue(false), Category("Appearance"), Description("Display a read-only path instead of allowing the user to type in a path.")] + public bool ReadOnlyPath { get; set; } + + /// Gets or sets the name of the remote network. + /// The name of the remote network. + [DefaultValue(null), Category("Behavior"), Description("The value displayed in the path field.")] + public string RemoteNetworkName { get => netRes.lpRemoteName; set => netRes.lpRemoteName = value; } + + /// Gets or sets a value indicating whether to enter the most recently used paths into the combination box. + /// true to use MRU path; otherwise, false. + /// UseMostRecentPath + [DefaultValue(false), Category("Behavior"), Description("Enter the most recently used paths into the combination box.")] + public bool UseMostRecentPath + { + get => dialogOptions.dwFlags.IsFlagSet(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_USE_MRU); + set + { + if (value && !string.IsNullOrEmpty(RemoteNetworkName)) + throw new InvalidOperationException($"{nameof(UseMostRecentPath)} cannot be set to true if {nameof(RemoteNetworkName)} has a value."); + + dialogOptions.dwFlags = dialogOptions.dwFlags.SetFlags(Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_USE_MRU, value); + } + } + + /// + public override void Reset() + { + dialogOptions.dwDevNum = -1; + dialogOptions.dwFlags = 0; + dialogOptions.lpConnRes = IntPtr.Zero; + ReadOnlyPath = false; + } + + /// + protected override bool RunDialog(IntPtr hwndOwner) + { + using var lpNetResource = Vanara.InteropServices.SafeCoTaskMemHandle.CreateFromStructure(netRes); + + dialogOptions.hwndOwner = hwndOwner; + dialogOptions.lpConnRes = lpNetResource.DangerousGetHandle(); + + if (ReadOnlyPath && !string.IsNullOrEmpty(netRes.lpRemoteName)) + dialogOptions.dwFlags |= Vanara.PInvoke.Mpr.CONN_DLG.CONNDLG_RO_PATH; + + var result = Vanara.PInvoke.Mpr.WNetConnectionDialog1(dialogOptions); + + dialogOptions.lpConnRes = IntPtr.Zero; + + if (result == unchecked((uint)-1)) + return false; + + result.ThrowIfFailed(); - return string.Empty; + return true; } } } diff --git a/src/Files.App/Services/NetworkDrivesService.cs b/src/Files.App/Services/NetworkDrivesService.cs index 00729e557136..a03fdc7bfc5f 100644 --- a/src/Files.App/Services/NetworkDrivesService.cs +++ b/src/Files.App/Services/NetworkDrivesService.cs @@ -13,6 +13,8 @@ namespace Files.App.Services { public sealed class NetworkDrivesService : ObservableObject, INetworkDrivesService { + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + private ObservableCollection _Drives; /// public ObservableCollection Drives @@ -130,17 +132,12 @@ public bool DisconnectNetworkDrive(ILocatableFolder drive) /// public Task OpenMapNetworkDriveDialogAsync() { - var hWnd = MainWindow.Instance.WindowHandle.ToInt64(); - return Win32Helper.StartSTATask(() => { - using var ncd = new NetworkConnectionDialog - { - UseMostRecentPath = true, - HideRestoreConnectionCheckBox = false - }; - - return ncd.ShowDialog(Win32Helper.Win32Window.FromLong(hWnd)) == System.Windows.Forms.DialogResult.OK; + return CommonDialogService.Open_NetworkConnectionDialog( + MainWindow.Instance.WindowHandle, + useMostRecentPath: true, + hideRestoreConnectionCheckBox: false); }); } diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 317e105abf57..45084fd795b2 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -82,7 +82,7 @@ private void ExecuteRestoreDefaultIconCommand() private void ExecuteOpenFilePickerCommand() { var parentWindowId = _appWindow.Id; - var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); + var hWnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); string[] extensions = [ @@ -91,7 +91,7 @@ private void ExecuteOpenFilePickerCommand() "ICO File", "*.ico", ]; - var filePath = CommonDialogService.Open_FileOpenDialog(handle, extensions); + CommonDialogService.Open_FileOpenDialog(hWnd, extensions, out var filePath); LoadIconsForPath(filePath); } diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index 36ccb3cdce31..5047e09d58ba 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -17,6 +17,7 @@ namespace Files.App.ViewModels.Settings public sealed class AdvancedViewModel : ObservableObject { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); @@ -204,11 +205,10 @@ private async Task ImportSettingsAsync() private async Task ExportSettingsAsync() { - FileSavePicker filePicker = InitializeWithWindow(new FileSavePicker()); - filePicker.FileTypeChoices.Add("Zip File", [".zip"]); - filePicker.SuggestedFileName = $"Files_{AppLifecycleHelper.AppVersion}"; + string[] extensions = [ "Zip File", "*.zip" ]; + CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, extensions, out var filePath); - StorageFile file = await filePicker.PickSaveFileAsync(); + var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) { try From e9bbf6485d5129ef09aba11af25254fadc9ae96e Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 03:16:49 +0900 Subject: [PATCH 04/22] Replace all --- .../Data/Contracts/ICommonDialogService.cs | 4 +- .../Data/Contracts/IFileExplorerService.cs | 15 ------ src/Files.App/Services/CommonDialogService.cs | 8 ++-- src/Files.App/Services/FileExplorerService.cs | 48 ------------------- .../Properties/BasePropertiesPage.cs | 21 ++++---- .../Properties/CustomizationViewModel.cs | 2 +- .../ViewModels/Settings/AdvancedViewModel.cs | 2 +- .../Settings/AppearanceViewModel.cs | 32 +++++++------ 8 files changed, 36 insertions(+), 96 deletions(-) diff --git a/src/Files.App/Data/Contracts/ICommonDialogService.cs b/src/Files.App/Data/Contracts/ICommonDialogService.cs index 41a5f727e3c6..b48270cfaab3 100644 --- a/src/Files.App/Data/Contracts/ICommonDialogService.cs +++ b/src/Files.App/Data/Contracts/ICommonDialogService.cs @@ -18,7 +18,7 @@ public interface ICommonDialogService /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. /// /// True if the 'Open' button was clicked; otherwise, false. - bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath); + bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); /// /// Opens a common dialog called FileSaveDialog through native Win32API. @@ -30,7 +30,7 @@ public interface ICommonDialogService /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. /// /// True if the 'Open' button was clicked; otherwise, false. - bool Open_FileSaveDialog(nint hWnd, string[] filters, out string filePath); + bool Open_FileSaveDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); /// /// Opens a common dialog called NetworkConnectionDialog through native Win32API. diff --git a/src/Files.App/Data/Contracts/IFileExplorerService.cs b/src/Files.App/Data/Contracts/IFileExplorerService.cs index 6c5008b1a008..773e1908371c 100644 --- a/src/Files.App/Data/Contracts/IFileExplorerService.cs +++ b/src/Files.App/Data/Contracts/IFileExplorerService.cs @@ -24,20 +24,5 @@ public interface IFileExplorerService /// A that cancels this action. /// A that represents the asynchronous operation. Task OpenInFileExplorerAsync(ILocatableFolder folder, CancellationToken cancellationToken = default); - - /// - /// Awaits the user input and picks single file from the file explorer dialog. - /// - /// The filter to apply when picking files. - /// A that cancels this action. - /// A that represents the asynchronous operation. If successful and a file has been picked, returns , otherwise null. - Task PickSingleFileAsync(IEnumerable? filter, CancellationToken cancellationToken = default); - - /// - /// Awaits the user input and picks single folder from the file explorer dialog. - /// - /// A that cancels this action. - /// A that represents the asynchronous operation. If successful and a folder has been picked, returns , otherwise null. - Task PickSingleFolderAsync(CancellationToken cancellationToken = default); } } diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index 63c970d80d71..a61040ebd2f3 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -17,7 +17,7 @@ namespace Files.App.Services public sealed class CommonDialogService : ICommonDialogService { /// - public bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath) + public bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) { filePath = string.Empty; @@ -54,7 +54,7 @@ public bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath // Get the default shell folder (My Computer) PInvoke.SHCreateItemFromParsingName( - Environment.GetFolderPath(Environment.SpecialFolder.MyComputer), + Environment.GetFolderPath(defaultFolder), null, typeof(IShellItem).GUID, out var directoryShellItem) @@ -84,7 +84,7 @@ public bool Open_FileOpenDialog(nint hWnd, string[] filters, out string filePath } /// - public bool Open_FileSaveDialog(nint hWnd, string[] filters, out string filePath) + public bool Open_FileSaveDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) { filePath = string.Empty; @@ -121,7 +121,7 @@ public bool Open_FileSaveDialog(nint hWnd, string[] filters, out string filePath // Get the default shell folder (My Computer) PInvoke.SHCreateItemFromParsingName( - Environment.GetFolderPath(Environment.SpecialFolder.MyComputer), + Environment.GetFolderPath(defaultFolder), null, typeof(IShellItem).GUID, out var directoryShellItem) diff --git a/src/Files.App/Services/FileExplorerService.cs b/src/Files.App/Services/FileExplorerService.cs index 9b69e1321c00..96efde0d2411 100644 --- a/src/Files.App/Services/FileExplorerService.cs +++ b/src/Files.App/Services/FileExplorerService.cs @@ -19,53 +19,5 @@ public Task OpenAppFolderAsync(CancellationToken cancellationToken = default) /// public Task OpenInFileExplorerAsync(ILocatableFolder folder, CancellationToken cancellationToken = default) => Launcher.LaunchFolderPathAsync(folder.Path).AsTask(cancellationToken); - - /// - public async Task PickSingleFileAsync(IEnumerable? filter, CancellationToken cancellationToken = default) - { - var filePicker = InitializeWithWindow(new FileOpenPicker()); - - if (filter is not null) - { - filePicker.FileTypeFilter.EnumeratedAdd(filter); - } - else - { - filePicker.FileTypeFilter.Add("*"); - } - - var fileTask = filePicker.PickSingleFileAsync().AsTask(cancellationToken); - var file = await fileTask; - - return file is null ? null : new WindowsStorageFile(file); - } - - // WINUI3 - private FileOpenPicker InitializeWithWindow(FileOpenPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - return obj; - } - - /// - public async Task PickSingleFolderAsync(CancellationToken cancellationToken = default) - { - var folderPicker = InitializeWithWindow(new FolderPicker()); - - folderPicker.FileTypeFilter.Add("*"); - - var folderTask = folderPicker.PickSingleFolderAsync().AsTask(cancellationToken); - var folder = await folderTask; - - return folder is null ? null : new WindowsStorageFolder(folder); - } - - // WINUI3 - private FolderPicker InitializeWithWindow(FolderPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - - return obj; - } } } diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 39e49d55de1b..438c7546a3dc 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -14,6 +14,8 @@ namespace Files.App.ViewModels.Properties { public abstract class BasePropertiesPage : Page, IDisposable { + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + public IShellPage AppInstance = null; public BaseProperties BaseProperties { get; set; } @@ -81,18 +83,17 @@ protected override void OnNavigatedTo(NavigationEventArgs e) ViewModel.EditAlbumCoverCommand = new RelayCommand(async () => { - FileOpenPicker filePicker = new FileOpenPicker(); - filePicker.FileTypeFilter.Add(".jpg"); - filePicker.FileTypeFilter.Add(".jpeg"); - filePicker.FileTypeFilter.Add(".bmp"); - filePicker.FileTypeFilter.Add(".png"); - - var parentWindowId = np.Window.AppWindow.Id; - var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); - WinRT.Interop.InitializeWithWindow.Initialize(filePicker, handle); + string[] extensions = + [ + "Image File", "*.jpg", + "Image File", "*.jpeg", + "Image File", "*.bmp", + "Image File", "*.png", + ]; - StorageFile file = await filePicker.PickSingleFileAsync(); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.Desktop, out var filePath); + var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) { ViewModel.IsAblumCoverModified = true; diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 45084fd795b2..89bb56e63854 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -91,7 +91,7 @@ private void ExecuteOpenFilePickerCommand() "ICO File", "*.ico", ]; - CommonDialogService.Open_FileOpenDialog(hWnd, extensions, out var filePath); + CommonDialogService.Open_FileOpenDialog(hWnd, extensions, Environment.SpecialFolder.MyComputer, out var filePath); LoadIconsForPath(filePath); } diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index 5047e09d58ba..cf0ffbfa5a96 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -206,7 +206,7 @@ private async Task ImportSettingsAsync() private async Task ExportSettingsAsync() { string[] extensions = [ "Zip File", "*.zip" ]; - CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, extensions, out var filePath); + CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.Desktop, out var filePath); var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index c2636af506c6..09d405919d64 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -14,6 +14,7 @@ namespace Files.App.ViewModels.Settings public sealed class AppearanceViewModel : ObservableObject { private IAppThemeModeService AppThemeModeService { get; } = Ioc.Default.GetRequiredService(); + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); private readonly IUserSettingsService UserSettingsService; private readonly IResourcesService ResourcesService; @@ -89,21 +90,22 @@ public AppearanceViewModel(IUserSettingsService userSettingsService, IResourcesS /// private async Task SelectBackgroundImage() { - var filePicker = new FileOpenPicker - { - ViewMode = PickerViewMode.Thumbnail, - SuggestedStartLocation = PickerLocationId.PicturesLibrary, - FileTypeFilter = { ".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp" } - }; - - // WINUI3: Create and initialize new window - var parentWindowId = MainWindow.Instance.AppWindow.Id; - var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId); - WinRT.Interop.InitializeWithWindow.Initialize(filePicker, handle); - - var file = await filePicker.PickSingleFileAsync(); - if (file is not null) - AppThemeBackgroundImageSource = file.Path; + string[] extensions = + [ + "Image File", "*.png", + "Image File", "*.bmp", + "Image File", "*.jpg", + "Image File", "*.jpeg", + "Image File", "*.jfif", + "Image File", "*.gif", + "Image File", "*.tiff", + "Image File", "*.tif", + "Image File", "*.webp", + ]; + + var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.MyPictures, out var filePath); + if (result) + AppThemeBackgroundImageSource = filePath; } /// From 1fa415afdbf90fb8d092ae42fff23facbbe99646 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 03:25:34 +0900 Subject: [PATCH 05/22] Update for folder picker --- src/Files.App/Data/Contracts/ICommonDialogService.cs | 6 ++++-- src/Files.App/Services/CommonDialogService.cs | 12 ++++++++++-- .../ViewModels/Properties/BasePropertiesPage.cs | 2 +- .../ViewModels/Properties/CustomizationViewModel.cs | 2 +- .../ViewModels/Settings/AdvancedViewModel.cs | 2 +- .../ViewModels/Settings/AppearanceViewModel.cs | 6 +++--- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Files.App/Data/Contracts/ICommonDialogService.cs b/src/Files.App/Data/Contracts/ICommonDialogService.cs index b48270cfaab3..7362a786e495 100644 --- a/src/Files.App/Data/Contracts/ICommonDialogService.cs +++ b/src/Files.App/Data/Contracts/ICommonDialogService.cs @@ -12,25 +12,27 @@ public interface ICommonDialogService /// Opens a common dialog called FileOpenDialog through native Win32API. /// /// The Window handle that the dialog launches based on. + /// The value that indicates whether the picker is only for folders. /// The extension filters that the dialog uses to exclude unnecessary files.
The filter must have a pair:[ "Application", ".exe" ] /// The file that that user chose. /// /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. /// /// True if the 'Open' button was clicked; otherwise, false. - bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); + bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); /// /// Opens a common dialog called FileSaveDialog through native Win32API. /// /// The Window handle that the dialog launches based on. + /// The value that indicates whether the picker is only for folders. /// The extension filters that the dialog uses to exclude unnecessary files.
The filter must have a pair:[ "Application", ".exe" ] /// The file that that user chose. /// /// NOTE: There's a WinRT API to launch this dialog, but the API doesn't support windows that are launched by those who is in Administrators group or has broader privileges. /// /// True if the 'Open' button was clicked; otherwise, false. - bool Open_FileSaveDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); + bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath); /// /// Opens a common dialog called NetworkConnectionDialog through native Win32API. diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index a61040ebd2f3..f0d01ed1bb77 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -17,7 +17,7 @@ namespace Files.App.Services public sealed class CommonDialogService : ICommonDialogService { /// - public bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) + public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) { filePath = string.Empty; @@ -60,6 +60,10 @@ public bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.Special out var directoryShellItem) .ThrowOnFailure(); + // Folder picker + if (pickFoldersOnly) + dialog.SetOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS); + // Set the default folder to open in the dialog dialog.SetFolder((IShellItem)directoryShellItem); dialog.SetDefaultFolder((IShellItem)directoryShellItem); @@ -84,7 +88,7 @@ public bool Open_FileOpenDialog(nint hWnd, string[] filters, Environment.Special } /// - public bool Open_FileSaveDialog(nint hWnd, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) + public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) { filePath = string.Empty; @@ -127,6 +131,10 @@ public bool Open_FileSaveDialog(nint hWnd, string[] filters, Environment.Special out var directoryShellItem) .ThrowOnFailure(); + // Folder picker + if (pickFoldersOnly) + dialog.SetOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS); + // Set the default folder to open in the dialog dialog.SetFolder((IShellItem)directoryShellItem); dialog.SetDefaultFolder((IShellItem)directoryShellItem); diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 438c7546a3dc..dd13e0e83ca2 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -91,7 +91,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) "Image File", "*.png", ]; - CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.Desktop, out var filePath); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 89bb56e63854..2455438da8da 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -91,7 +91,7 @@ private void ExecuteOpenFilePickerCommand() "ICO File", "*.ico", ]; - CommonDialogService.Open_FileOpenDialog(hWnd, extensions, Environment.SpecialFolder.MyComputer, out var filePath); + CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.MyComputer, out var filePath); LoadIconsForPath(filePath); } diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index cf0ffbfa5a96..d56ee71c5f40 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -206,7 +206,7 @@ private async Task ImportSettingsAsync() private async Task ExportSettingsAsync() { string[] extensions = [ "Zip File", "*.zip" ]; - CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.Desktop, out var filePath); + CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index 09d405919d64..1a21eda591fa 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -81,14 +81,14 @@ public AppearanceViewModel(IUserSettingsService userSettingsService, IResourcesS UpdateSelectedResource(); - SelectImageCommand = new AsyncRelayCommand(SelectBackgroundImage); + SelectImageCommand = new RelayCommand(SelectBackgroundImage); RemoveImageCommand = new RelayCommand(RemoveBackgroundImage); } /// /// Opens a file picker to select a background image /// - private async Task SelectBackgroundImage() + private void SelectBackgroundImage() { string[] extensions = [ @@ -103,7 +103,7 @@ private async Task SelectBackgroundImage() "Image File", "*.webp", ]; - var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, extensions, Environment.SpecialFolder.MyPictures, out var filePath); + var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, out var filePath); if (result) AppThemeBackgroundImageSource = filePath; } From 4634de67287036833dd1fd10725954faa8755164 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 03:37:15 +0900 Subject: [PATCH 06/22] Replace all 2 --- .../Dialogs/CreateShortcutDialogViewModel.cs | 6 ----- .../DecompressArchiveDialogViewModel.cs | 15 +++-------- .../ViewModels/Settings/GeneralViewModel.cs | 27 +++++-------------- .../Views/Properties/LibraryPage.xaml.cs | 15 ++++------- 4 files changed, 15 insertions(+), 48 deletions(-) diff --git a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs index 3021aa78a99c..827f4c9d2758 100644 --- a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs @@ -108,12 +108,6 @@ private Task SelectDestination() return Task.CompletedTask; } - private FolderPicker InitializeWithWindow(FolderPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - return obj; - } - private async Task CreateShortcutAsync() { string? destinationName; diff --git a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs index 64d410ef3840..4d18c9f98203 100644 --- a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs @@ -10,6 +10,8 @@ namespace Files.App.ViewModels.Dialogs { public sealed class DecompressArchiveDialogViewModel : ObservableObject { + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + private readonly IStorageFile archive; public StorageFolder DestinationFolder { get; private set; } @@ -60,21 +62,12 @@ public DecompressArchiveDialogViewModel(IStorageFile archive) private async Task SelectDestinationAsync() { - FolderPicker folderPicker = InitializeWithWindow(new FolderPicker()); - folderPicker.FileTypeFilter.Add("*"); - - DestinationFolder = await folderPicker.PickSingleFolderAsync(); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); + DestinationFolder = await StorageHelpers.ToStorageItem(filePath); DestinationFolderPath = (DestinationFolder is not null) ? DestinationFolder.Path : DefaultDestinationFolderPath(); } - // WINUI3 - private FolderPicker InitializeWithWindow(FolderPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - return obj; - } - private string DefaultDestinationFolderPath() { return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path)); diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index cfa12ba59c89..dce0bfeffeed 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -15,6 +15,7 @@ namespace Files.App.ViewModels.Settings public sealed class GeneralViewModel : ObservableObject, IDisposable { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); private bool disposed; @@ -317,23 +318,10 @@ public bool AlwaysOpenDualPaneInNewTab private async Task ChangePageAsync() { - var folderPicker = InitializeWithWindow(new FolderPicker()); - folderPicker.FileTypeFilter.Add("*"); - StorageFolder folder = await folderPicker.PickSingleFolderAsync(); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); - if (folder is not null) - { - if (SelectedPageIndex >= 0) - PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(folder.Path); - } - } - - // WINUI3 - private FolderPicker InitializeWithWindow(FolderPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - - return obj; + if (SelectedPageIndex >= 0) + PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(filePath); } private void RemovePage(PageOnStartupViewModel page) @@ -345,12 +333,9 @@ private async Task AddPageAsync(string path = null) { if (string.IsNullOrWhiteSpace(path)) { - var folderPicker = InitializeWithWindow(new FolderPicker()); - folderPicker.FileTypeFilter.Add("*"); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); - var folder = await folderPicker.PickSingleFolderAsync(); - if (folder is not null) - path = folder.Path; + path = filePath; } if (path is not null && PagesOnStartupList is not null) diff --git a/src/Files.App/Views/Properties/LibraryPage.xaml.cs b/src/Files.App/Views/Properties/LibraryPage.xaml.cs index 3eccdf7d8e9f..5901f156b3d5 100644 --- a/src/Files.App/Views/Properties/LibraryPage.xaml.cs +++ b/src/Files.App/Views/Properties/LibraryPage.xaml.cs @@ -16,11 +16,14 @@ using System.Threading.Tasks; using System.Windows.Input; using Windows.Storage.Pickers; +using Windows.Storage; namespace Files.App.Views.Properties { public sealed partial class LibraryPage : BasePropertiesPage, INotifyPropertyChanged { + private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") @@ -100,10 +103,9 @@ protected override void Properties_Loaded(object sender, RoutedEventArgs e) private async Task AddLocationAsync() { - var folderPicker = InitializeWithWindow(new FolderPicker()); - folderPicker.FileTypeFilter.Add("*"); + CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); - var folder = await folderPicker.PickSingleFolderAsync(); + var folder = await StorageHelpers.ToStorageItem(filePath); if (folder is not null && !Folders.Any((f) => string.Equals(folder.Path, f.Path, StringComparison.OrdinalIgnoreCase))) { bool isDefault = Folders.Count == 0; @@ -115,13 +117,6 @@ private async Task AddLocationAsync() } } - // WINUI3 - private FolderPicker InitializeWithWindow(FolderPicker obj) - { - WinRT.Interop.InitializeWithWindow.Initialize(obj, MainWindow.Instance.WindowHandle); - return obj; - } - private void SetDefaultLocation() { int index = SelectedFolderIndex; From a1bd6e34514cc7afdaed22688ede302d2ffb42a2 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 03:58:42 +0900 Subject: [PATCH 07/22] Fix a build issue --- src/Files.App/Services/CommonDialogService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index f0d01ed1bb77..dc599714cddd 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -88,7 +88,7 @@ public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filter } /// - public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) + public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath) { filePath = string.Empty; From 132e1c3bd22621e8091750c46c2e9b1ee11c1fb6 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 13 May 2024 04:26:47 +0900 Subject: [PATCH 08/22] Added result check --- .../ViewModels/Properties/CustomizationViewModel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 2455438da8da..1e237fa71b5e 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -91,9 +91,10 @@ private void ExecuteOpenFilePickerCommand() "ICO File", "*.ico", ]; - CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.MyComputer, out var filePath); + var result = CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.MyComputer, out var filePath); - LoadIconsForPath(filePath); + if (result) + LoadIconsForPath(filePath); } public async Task UpdateIcon() From a9c4d847eed2c886c1fd5de8b9b5cdb055b1a51e Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 07:23:49 +0900 Subject: [PATCH 09/22] Update --- src/Files.App/Services/CommonDialogService.cs | 2 ++ .../ViewModels/Properties/CustomizationViewModel.cs | 1 - src/Files.App/ViewModels/Settings/GeneralViewModel.cs | 11 +++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index dc599714cddd..0478a1525695 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -62,7 +62,9 @@ public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filter // Folder picker if (pickFoldersOnly) + { dialog.SetOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS); + } // Set the default folder to open in the dialog dialog.SetFolder((IShellItem)directoryShellItem); diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 1e237fa71b5e..8f1236058337 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -92,7 +92,6 @@ private void ExecuteOpenFilePickerCommand() ]; var result = CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.MyComputer, out var filePath); - if (result) LoadIconsForPath(filePath); } diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index dce0bfeffeed..a37ec098d1c3 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -21,7 +21,7 @@ public sealed class GeneralViewModel : ObservableObject, IDisposable private ReadOnlyCollection addFlyoutItemsSource; - public AsyncRelayCommand ChangePageCommand { get; } + public RelayCommand ChangePageCommand { get; } public RelayCommand RemovePageCommand { get; } public RelayCommand AddPageCommand { get; } public RelayCommand RestartCommand { get; } @@ -90,7 +90,7 @@ public int SelectedAppLanguageIndex public GeneralViewModel() { - ChangePageCommand = new AsyncRelayCommand(ChangePageAsync); + ChangePageCommand = new RelayCommand(ChangePageAsync); RemovePageCommand = new RelayCommand(RemovePage); AddPageCommand = new RelayCommand(async (path) => await AddPageAsync(path)); RestartCommand = new RelayCommand(DoRestartAsync); @@ -316,11 +316,10 @@ public bool AlwaysOpenDualPaneInNewTab } } - private async Task ChangePageAsync() + private void ChangePageAsync() { - CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); - - if (SelectedPageIndex >= 0) + var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); + if (result && SelectedPageIndex >= 0) PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(filePath); } From fb8a15b43846f6b2142e6f03e04a8049de5be1ba Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:13:23 +0900 Subject: [PATCH 10/22] Fix --- src/Files.App/Services/CommonDialogService.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Files.App/Services/CommonDialogService.cs b/src/Files.App/Services/CommonDialogService.cs index 0478a1525695..3b7d19d868ea 100644 --- a/src/Files.App/Services/CommonDialogService.cs +++ b/src/Files.App/Services/CommonDialogService.cs @@ -33,10 +33,10 @@ public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filter out IFileOpenDialog dialog) .ThrowOnFailure(); - List extensions = []; - if (filters.Length is not 0 && filters.Length % 2 is 0) { + List extensions = []; + for (int i = 1; i < filters.Length; i += 2) { COMDLG_FILTERSPEC extension; @@ -47,10 +47,10 @@ public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filter // Add to the exclusive extension list extensions.Add(extension); } - } - // Set the file type using the extension list - dialog.SetFileTypes(extensions.ToArray()); + // Set the file type using the extension list + dialog.SetFileTypes(extensions.ToArray()); + } // Get the default shell folder (My Computer) PInvoke.SHCreateItemFromParsingName( @@ -106,10 +106,10 @@ public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filter out IFileSaveDialog dialog) .ThrowOnFailure(); - List extensions = []; - if (filters.Length is not 0 && filters.Length % 2 is 0) { + List extensions = []; + for (int i = 1; i < filters.Length; i += 2) { COMDLG_FILTERSPEC extension; @@ -120,10 +120,10 @@ public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filter // Add to the exclusive extension list extensions.Add(extension); } - } - // Set the file type using the extension list - dialog.SetFileTypes(extensions.ToArray()); + // Set the file type using the extension list + dialog.SetFileTypes(extensions.ToArray()); + } // Get the default shell folder (My Computer) PInvoke.SHCreateItemFromParsingName( From 99d9ef26ec1fcb977718fb1a983fd9aac06f5aee Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:16:29 +0900 Subject: [PATCH 11/22] Fix 2 --- src/Files.App/ViewModels/Properties/BasePropertiesPage.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index dd13e0e83ca2..d8e42a0e08fa 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -83,6 +83,8 @@ protected override void OnNavigatedTo(NavigationEventArgs e) ViewModel.EditAlbumCoverCommand = new RelayCommand(async () => { + var hWnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(np.Window.AppWindow.Id); + string[] extensions = [ "Image File", "*.jpg", @@ -91,7 +93,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) "Image File", "*.png", ]; - CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); + CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); var file = await StorageHelpers.ToStorageItem(filePath); if (file is not null) From 26e53538c0818370dbce9f841989d957cad3db60 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:30:43 +0900 Subject: [PATCH 12/22] Localize --- src/Files.App/Strings/en-US/Resources.resw | 16 ++++++++++++++++ .../Properties/BasePropertiesPage.cs | 8 ++++---- .../Properties/CustomizationViewModel.cs | 6 +++--- .../ViewModels/Settings/AdvancedViewModel.cs | 2 +- .../ViewModels/Settings/AppearanceViewModel.cs | 18 +++++++++--------- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 56cb01e10418..9eee6bc3fd87 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3810,4 +3810,20 @@ Show for Git repos Setting where users can choose to display "Open IDE" button for all locations. + + Application extension + This is the friendly name of DLL file. + + + ICO File + This is the friendly type name of ICO file. + + + Zip File + This is the friendly type name of ZIP file. + + + Image File + This is the friendly type name of image files group. + \ No newline at end of file diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index d8e42a0e08fa..8dbbb44d657a 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -87,10 +87,10 @@ protected override void OnNavigatedTo(NavigationEventArgs e) string[] extensions = [ - "Image File", "*.jpg", - "Image File", "*.jpeg", - "Image File", "*.bmp", - "Image File", "*.png", + "ImageFileCapitalized".GetLocalizedResource(), "*.jpg", + "ImageFileCapitalized".GetLocalizedResource(), "*.jpeg", + "ImageFileCapitalized".GetLocalizedResource(), "*.bmp", + "ImageFileCapitalized".GetLocalizedResource(), "*.png", ]; CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index 8f1236058337..c0b704129c87 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -86,9 +86,9 @@ private void ExecuteOpenFilePickerCommand() string[] extensions = [ - "Application extension", "*.dll", - "Application", "*.exe", - "ICO File", "*.ico", + "ApplicationExtension".GetLocalizedResource(), "*.dll", + "Application".GetLocalizedResource(), "*.exe", + "IcoFileCapitalized".GetLocalizedResource(), "*.ico", ]; var result = CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.MyComputer, out var filePath); diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index d56ee71c5f40..f67dde9f838a 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -205,7 +205,7 @@ private async Task ImportSettingsAsync() private async Task ExportSettingsAsync() { - string[] extensions = [ "Zip File", "*.zip" ]; + string[] extensions = ["ZipFileCapitalized".GetLocalizedResource(), "*.zip" ]; CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); var file = await StorageHelpers.ToStorageItem(filePath); diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index 1a21eda591fa..d3fe562b8323 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -92,15 +92,15 @@ private void SelectBackgroundImage() { string[] extensions = [ - "Image File", "*.png", - "Image File", "*.bmp", - "Image File", "*.jpg", - "Image File", "*.jpeg", - "Image File", "*.jfif", - "Image File", "*.gif", - "Image File", "*.tiff", - "Image File", "*.tif", - "Image File", "*.webp", + "ImageFileCapitalized".GetLocalizedResource(), "*.png", + "ImageFileCapitalized".GetLocalizedResource(), "*.bmp", + "ImageFileCapitalized".GetLocalizedResource(), "*.jpg", + "ImageFileCapitalized".GetLocalizedResource(), "*.jpeg", + "ImageFileCapitalized".GetLocalizedResource(), "*.jfif", + "ImageFileCapitalized".GetLocalizedResource(), "*.gif", + "ImageFileCapitalized".GetLocalizedResource(), "*.tiff", + "ImageFileCapitalized".GetLocalizedResource(), "*.tif", + "ImageFileCapitalized".GetLocalizedResource(), "*.webp", ]; var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, out var filePath); From 09d898230a7a6fc3ea1f6b9849589f197ffa812f Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:32:02 +0900 Subject: [PATCH 13/22] Update src/Files.App/Strings/en-US/Resources.resw Co-authored-by: Yair <39923744+yaira2@users.noreply.github.com> --- src/Files.App/Strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 9eee6bc3fd87..776bc98c1e7b 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3812,7 +3812,7 @@ Application extension - This is the friendly name of DLL file. + This is the friendly name for DLL files. ICO File From 6034add4e00956693d22151e7123aadcfba6be79 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:33:17 +0900 Subject: [PATCH 14/22] Update src/Files.App/Strings/en-US/Resources.resw Co-authored-by: Yair <39923744+yaira2@users.noreply.github.com> --- src/Files.App/Strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 776bc98c1e7b..0aaff764e8ba 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3816,7 +3816,7 @@ ICO File - This is the friendly type name of ICO file. + This is the friendly name for ICO files. Zip File From efbc6f2332684e3da619bf28a024518992f53d59 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:35:39 +0900 Subject: [PATCH 15/22] Update src/Files.App/Strings/en-US/Resources.resw Co-authored-by: Yair <39923744+yaira2@users.noreply.github.com> --- src/Files.App/Strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 0aaff764e8ba..8dacfe5936dd 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3820,7 +3820,7 @@ Zip File - This is the friendly type name of ZIP file. + This is the friendly name for ZIP files. Image File From 4b4fe39c2229048a85889723ccf1b183f4138d81 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 08:35:47 +0900 Subject: [PATCH 16/22] Update src/Files.App/Strings/en-US/Resources.resw Co-authored-by: Yair <39923744+yaira2@users.noreply.github.com> --- src/Files.App/Strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 8dacfe5936dd..728a6ea67b7f 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3824,6 +3824,6 @@ Image File - This is the friendly type name of image files group. + This is the friendly name for image files. \ No newline at end of file From 94dee29a51ca7b7b76167904954064df414d483a Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Thu, 16 May 2024 09:10:18 +0900 Subject: [PATCH 17/22] Update --- .../ViewModels/Properties/BasePropertiesPage.cs | 5 +---- .../ViewModels/Settings/AppearanceViewModel.cs | 10 +--------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 8dbbb44d657a..cb2f05d8f0b1 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -87,10 +87,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) string[] extensions = [ - "ImageFileCapitalized".GetLocalizedResource(), "*.jpg", - "ImageFileCapitalized".GetLocalizedResource(), "*.jpeg", - "ImageFileCapitalized".GetLocalizedResource(), "*.bmp", - "ImageFileCapitalized".GetLocalizedResource(), "*.png", + "ImageFileCapitalized".GetLocalizedResource(), "*.jpg;*.jpeg;*.bmp;*.png", ]; CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index d3fe562b8323..1466227e0a43 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -92,15 +92,7 @@ private void SelectBackgroundImage() { string[] extensions = [ - "ImageFileCapitalized".GetLocalizedResource(), "*.png", - "ImageFileCapitalized".GetLocalizedResource(), "*.bmp", - "ImageFileCapitalized".GetLocalizedResource(), "*.jpg", - "ImageFileCapitalized".GetLocalizedResource(), "*.jpeg", - "ImageFileCapitalized".GetLocalizedResource(), "*.jfif", - "ImageFileCapitalized".GetLocalizedResource(), "*.gif", - "ImageFileCapitalized".GetLocalizedResource(), "*.tiff", - "ImageFileCapitalized".GetLocalizedResource(), "*.tif", - "ImageFileCapitalized".GetLocalizedResource(), "*.webp", + "ImageFileCapitalized".GetLocalizedResource(), "*.png;*.bmp;*.jpg;*.jpeg;*.jfif;*.gif;*.tiff;*.tif;*.webp", ]; var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, out var filePath); From 8cac9a4695aeda923550e7208f8a8eb20f620093 Mon Sep 17 00:00:00 2001 From: Yair <39923744+yaira2@users.noreply.github.com> Date: Thu, 16 May 2024 12:45:20 -0400 Subject: [PATCH 18/22] Improve file format groups --- src/Files.App/Strings/en-US/Resources.resw | 6 +++--- .../ViewModels/Properties/BasePropertiesPage.cs | 10 +++++++--- .../ViewModels/Settings/AppearanceViewModel.cs | 8 +++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 728a6ea67b7f..536cf9cc3678 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3822,8 +3822,8 @@ Zip File This is the friendly name for ZIP files. - - Image File - This is the friendly name for image files. + + Bitmap Files + This is the friendly name for bitmap files. \ No newline at end of file diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index cb2f05d8f0b1..b70fdadaf055 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -7,8 +7,6 @@ using Microsoft.UI.Xaml.Navigation; using TagLib; using Windows.Storage; -using Windows.Storage.FileProperties; -using Windows.Storage.Pickers; namespace Files.App.ViewModels.Properties { @@ -87,7 +85,13 @@ protected override void OnNavigatedTo(NavigationEventArgs e) string[] extensions = [ - "ImageFileCapitalized".GetLocalizedResource(), "*.jpg;*.jpeg;*.bmp;*.png", + "BitmapFiles".GetLocalizedResource(), "*.bmp;*.dib", + "JPEG", "*.jpg;*.jpeg;*.jpe;*.jfif", + "GIF", "*.gif", + "TIFF", "*.tif;*.tiff", + "PNG", "*.png", + "HEIC", "*.heic;*.hif", + "WEBP", "*.webp", ]; CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index 1466227e0a43..450edf51748a 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -92,7 +92,13 @@ private void SelectBackgroundImage() { string[] extensions = [ - "ImageFileCapitalized".GetLocalizedResource(), "*.png;*.bmp;*.jpg;*.jpeg;*.jfif;*.gif;*.tiff;*.tif;*.webp", + "BitmapFiles".GetLocalizedResource(), "*.bmp;*.dib", + "JPEG", "*.jpg;*.jpeg;*.jpe;*.jfif", + "GIF", "*.gif", + "TIFF", "*.tif;*.tiff", + "PNG", "*.png", + "HEIC", "*.heic;*.hif", + "WEBP", "*.webp", ]; var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, out var filePath); From 6a4e34bb7707c7817ff50b94a31af097c1b53f88 Mon Sep 17 00:00:00 2001 From: Yair <39923744+yaira2@users.noreply.github.com> Date: Thu, 16 May 2024 15:56:48 -0400 Subject: [PATCH 19/22] Update BasePropertiesPage.cs --- src/Files.App/ViewModels/Properties/BasePropertiesPage.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index b70fdadaf055..3f63b3119c52 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -85,13 +85,9 @@ protected override void OnNavigatedTo(NavigationEventArgs e) string[] extensions = [ - "BitmapFiles".GetLocalizedResource(), "*.bmp;*.dib", - "JPEG", "*.jpg;*.jpeg;*.jpe;*.jfif", - "GIF", "*.gif", - "TIFF", "*.tif;*.tiff", + "BitmapFiles".GetLocalizedResource(), "*.bmp", + "JPEG", "*.jpg;*.jpeg;*.jpe", "PNG", "*.png", - "HEIC", "*.heic;*.hif", - "WEBP", "*.webp", ]; CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); From 94bbbffe896ec8c1f8022d63d2859e422a324bd4 Mon Sep 17 00:00:00 2001 From: Yair <39923744+yaira2@users.noreply.github.com> Date: Thu, 16 May 2024 15:57:27 -0400 Subject: [PATCH 20/22] Update BasePropertiesPage.cs --- src/Files.App/ViewModels/Properties/BasePropertiesPage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 3f63b3119c52..a63dd1d3eb15 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -86,7 +86,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) string[] extensions = [ "BitmapFiles".GetLocalizedResource(), "*.bmp", - "JPEG", "*.jpg;*.jpeg;*.jpe", + "JPEG", "*.jpg;*.jpeg", "PNG", "*.png", ]; From 6e4da084b792e403a544d2c0d4b1e018f9d1809e Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Fri, 17 May 2024 08:47:30 +0900 Subject: [PATCH 21/22] Result check --- .../ViewModels/Properties/BasePropertiesPage.cs | 11 +++++------ src/Files.App/ViewModels/Settings/GeneralViewModel.cs | 4 ++-- src/Files.App/Views/Properties/LibraryPage.xaml.cs | 4 +++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index cb2f05d8f0b1..f42bc47d2320 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -90,19 +90,18 @@ protected override void OnNavigatedTo(NavigationEventArgs e) "ImageFileCapitalized".GetLocalizedResource(), "*.jpg;*.jpeg;*.bmp;*.png", ]; - CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); - - var file = await StorageHelpers.ToStorageItem(filePath); - if (file is not null) + var result = CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath); + if (result) { ViewModel.IsAblumCoverModified = true; - ViewModel.ModifiedAlbumCover = new Picture(file.Path); + ViewModel.ModifiedAlbumCover = new Picture(filePath); var result = await FileThumbnailHelper.GetIconAsync( - file.Path, + filePath, Constants.ShellIconSizes.ExtraLarge, false, IconOptions.UseCurrentScale); + ViewModel.IconData = result; } }); diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index a37ec098d1c3..628261675472 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -319,7 +319,7 @@ public bool AlwaysOpenDualPaneInNewTab private void ChangePageAsync() { var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); - if (result && SelectedPageIndex >= 0) + if (result && SelectedPageIndex >= 0) PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(filePath); } @@ -337,7 +337,7 @@ private async Task AddPageAsync(string path = null) path = filePath; } - if (path is not null && PagesOnStartupList is not null) + if (!string.IsNullOrEmpty(path) && PagesOnStartupList is not null) PagesOnStartupList.Add(new PageOnStartupViewModel(path)); } diff --git a/src/Files.App/Views/Properties/LibraryPage.xaml.cs b/src/Files.App/Views/Properties/LibraryPage.xaml.cs index 5901f156b3d5..e00ec2f4d373 100644 --- a/src/Files.App/Views/Properties/LibraryPage.xaml.cs +++ b/src/Files.App/Views/Properties/LibraryPage.xaml.cs @@ -103,7 +103,9 @@ protected override void Properties_Loaded(object sender, RoutedEventArgs e) private async Task AddLocationAsync() { - CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); + var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath); + if (!result) + return; var folder = await StorageHelpers.ToStorageItem(filePath); if (folder is not null && !Folders.Any((f) => string.Equals(folder.Path, f.Path, StringComparison.OrdinalIgnoreCase))) From a2cd577d60be441c1a682d533a75825805b95b10 Mon Sep 17 00:00:00 2001 From: 0x5bfa <62196528+0x5bfa@users.noreply.github.com> Date: Fri, 17 May 2024 08:51:16 +0900 Subject: [PATCH 22/22] Fix --- src/Files.App/ViewModels/Properties/BasePropertiesPage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 06089180263f..74b833b3ad51 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -96,13 +96,13 @@ protected override void OnNavigatedTo(NavigationEventArgs e) ViewModel.IsAblumCoverModified = true; ViewModel.ModifiedAlbumCover = new Picture(filePath); - var result = await FileThumbnailHelper.GetIconAsync( + var iconData = await FileThumbnailHelper.GetIconAsync( filePath, Constants.ShellIconSizes.ExtraLarge, false, IconOptions.UseCurrentScale); - ViewModel.IconData = result; + ViewModel.IconData = iconData; } });