Merge branch 'dev' into file_tooltip

This commit is contained in:
Jack Ye 2025-06-06 20:09:02 +08:00 committed by GitHub
commit d0c240f50f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1004 additions and 812 deletions

View file

@ -2407,6 +2407,79 @@
</Setter.Value>
</Setter>
</Style>
<!-- Explorer Plugin Expander -->
<Style x:Key="ExpanderHeaderRightArrowStyle" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="RootBorder" Background="Transparent" Padding="16,15,16,15">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
Grid.Column="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
RecognizesAccessKey="True"
SnapsToDevicePixels="True"
Content="{TemplateBinding Content}"
Margin="8 0 0 0"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Grid Grid.Column="1"
Width="20" Height="20"
Margin="8 0 4 0"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Background="Transparent"
RenderTransformOrigin="0.5,0.5"
x:Name="ChevronGrid">
<Grid.RenderTransform>
<RotateTransform Angle="0"/>
</Grid.RenderTransform>
<Ellipse
x:Name="circle"
Width="19"
Height="19"
Stroke="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Path
x:Name="arrow"
Data="M 1,1.5 L 4.5,5 L 8,1.5"
Stroke="#666"
StrokeThickness="1"
SnapsToDevicePixels="False"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="arrow" Property="Data" Value="M 1,4.5 L 4.5,1 L 8,4.5" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RootBorder" Property="Background" Value="{DynamicResource CustomExpanderHover}" />
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color05B}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="circle" Property="StrokeThickness" Value="1.5" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color17B}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ExpanderStyle1" TargetType="{x:Type Expander}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Background" Value="Transparent" />

View file

@ -270,6 +270,7 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
/// <summary>
/// Path to places.sqlite
/// </summary>
/// <remarks></remarks>
private static string PlacesPath
{
get
@ -295,12 +296,50 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName);
/*
Current profiles.ini structure example as of Firefox version 69.0.1
[Install736426B0AF4A39CB]
Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
Locked=1
[Profile2]
Name=newblahprofile
IsRelative=0
Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
[Profile1]
Name=default
IsRelative=1
Path=Profiles/cydum7q4.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/7789f565.default-release
[General]
StartWithLastProfile=1
Version=2
*/
// Seen in the example above, the IsRelative attribute is always above the Path attribute
var relativePath = Path.Combine(defaultProfileFolderName, "places.sqlite");
var absoluePath = Path.Combine(profileFolderPath, relativePath);
// If the index is out of range, it means that the default profile is in a custom location or the file is malformed
// If the profile is in a custom location, we need to check
if (indexOfDefaultProfileAttributePath - 1 < 0 ||
indexOfDefaultProfileAttributePath - 1 >= lines.Count)
{
return Directory.Exists(absoluePath) ? absoluePath : relativePath;
}
var relativeAttribute = lines[indexOfDefaultProfileAttributePath - 1];
return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0
? defaultProfileFolderName + @"\places.sqlite"
: Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite";
? relativePath : absoluePath;
}
}
}

View file

@ -166,6 +166,9 @@
<system:String x:Key="flowlauncher_plugin_everything_enable_content_search">Do you want to enable content search for Everything?</system:String>
<system:String x:Key="flowlauncher_plugin_everything_enable_content_search_tips">It can be very slow without index (which is only supported in Everything v1.5+)</system:String>
<system:String x:Key="flowlauncher_plugin_everything_not_found">Unable to find Everything.exe</system:String>
<system:String x:Key="flowlauncher_plugin_everything_install_issue">Failed to install Everything, please install it manually</system:String>
<!-- Native Context Menu -->
<system:String x:Key="plugin_explorer_native_context_menu_header">Native Context Menu</system:String>
<system:String x:Key="plugin_explorer_native_context_menu_display_context_menu">Display native context menu (experimental)</system:String>

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public class EverythingSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
{
private static readonly string ClassName = nameof(EverythingSearchManager);
private Settings Settings { get; }
public EverythingSearchManager(Settings settings)
@ -42,19 +44,32 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
{
var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
if (installedPath == null)
try
{
Main.Context.API.ShowMsgError("Unable to find Everything.exe");
var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
if (installedPath == null)
{
Main.Context.API.ShowMsgError(Main.Context.API.GetTranslation("flowlauncher_plugin_everything_not_found"));
Main.Context.API.LogError(ClassName, "Unable to find Everything.exe");
return false;
}
Settings.EverythingInstalledPath = installedPath;
Process.Start(installedPath, "-startup");
return true;
}
// Sometimes Everything installation will fail because of permission issues or file not found issues
// Just let the user know that Everything is not installed properly and ask them to install it manually
catch (Exception e)
{
Main.Context.API.ShowMsgError(Main.Context.API.GetTranslation("flowlauncher_plugin_everything_install_issue"));
Main.Context.API.LogException(ClassName, "Failed to install Everything", e);
return false;
}
Settings.EverythingInstalledPath = installedPath;
Process.Start(installedPath, "-startup");
return true;
}
public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [EnumeratorCancellation] CancellationToken token)

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
@ -11,28 +12,32 @@ using DragEventArgs = System.Windows.DragEventArgs;
namespace Flow.Launcher.Plugin.Explorer.Views
{
/// <summary>
/// Interaction logic for ExplorerSettings.xaml
/// </summary>
public partial class ExplorerSettings
{
private readonly SettingsViewModel viewModel;
private readonly SettingsViewModel _viewModel;
private readonly List<Expander> _expanders;
public ExplorerSettings(SettingsViewModel viewModel)
{
_viewModel = viewModel;
DataContext = viewModel;
InitializeComponent();
this.viewModel = viewModel;
DataContext = viewModel;
ActionKeywordModel.Init(viewModel.Settings);
lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
_expanders = new List<Expander>
{
GeneralSettingsExpander,
ContextMenuExpander,
PreviewPanelExpander,
EverythingExpander,
ActionKeywordsExpander,
QuickAccessExpander,
ExcludedPathsExpander
};
}
private void AccessLinkDragDrop(string containerName, DragEventArgs e)
@ -51,7 +56,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
Path = s
};
viewModel.AppendLink(containerName, newFolderLink);
_viewModel.AppendLink(containerName, newFolderLink);
}
}
}
@ -76,8 +81,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
if (tbFastSortWarning is not null)
{
tbFastSortWarning.Visibility = viewModel.FastSortWarningVisibility;
tbFastSortWarning.Text = viewModel.SortOptionWarningMessage;
tbFastSortWarning.Visibility = _viewModel.FastSortWarningVisibility;
tbFastSortWarning.Text = _viewModel.SortOptionWarningMessage;
}
}
private void LbxAccessLinks_OnDrop(object sender, DragEventArgs e)
@ -93,5 +98,32 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
e.Handled = e.Text.ToCharArray().Any(c => !char.IsDigit(c));
}
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
if (sender is Expander expandedExpander)
{
// Ensure _expanders is not null and contains items
if (_expanders == null || !_expanders.Any()) return;
foreach (var expander in _expanders)
{
if (expander != null && expander != expandedExpander && expander.IsExpanded)
{
expander.IsExpanded = false;
}
}
}
}
private void lbxAccessLinks_Loaded(object sender, RoutedEventArgs e)
{
lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
private void lbxExcludedPaths_Loaded(object sender, RoutedEventArgs e)
{
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
}
}

View file

@ -8,9 +8,8 @@ using Windows.Win32.Storage.FileSystem;
namespace Flow.Launcher.Plugin.Program.Programs
{
class ShellLinkHelper
public class ShellLinkHelper
{
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
[ComImport(), Guid("00021401-0000-0000-C000-000000000046")]
public class ShellLink
@ -28,7 +27,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
const int STGM_READ = 0;
((IPersistFile)link).Load(path, STGM_READ);
var hwnd = new HWND(IntPtr.Zero);
((IShellLinkW)link).Resolve(hwnd, 0);
// Use SLR_NO_UI to avoid showing any UI during resolution, like Problem with Shortcut dialogs
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-resolve
((IShellLinkW)link).Resolve(hwnd, (uint)SLR_FLAGS.SLR_NO_UI);
const int MAX_PATH = 260;
Span<char> buffer = stackalloc char[MAX_PATH];
@ -79,6 +80,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
Marshal.ReleaseComObject(link);
return target;
}
}
}
}

View file

@ -201,94 +201,101 @@ namespace Flow.Launcher.Plugin.Shell
switch (_settings.Shell)
{
case Shell.Cmd:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("cmd");
}
else
{
info.FileName = "cmd.exe";
}
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("cmd");
}
else
{
info.FileName = "cmd.exe";
}
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
case Shell.Powershell:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("powershell");
// Using just a ; doesn't work with wt, as it's used to create a new tab for the terminal window
// \\ must be escaped for it to work properly, or breaking it into multiple arguments
var addedCharacter = _settings.UseWindowsTerminal ? "\\" : "";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("powershell");
}
else
{
info.FileName = "powershell.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
info.ArgumentList.Add(command);
}
else
{
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}{addedCharacter}; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'{addedCharacter}; [System.Console]::ReadKey(){addedCharacter}; exit" : "")}");
}
break;
}
else
{
info.FileName = "powershell.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
info.ArgumentList.Add(command);
}
else
{
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
}
break;
}
case Shell.Pwsh:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("pwsh");
// Using just a ; doesn't work with wt, as it's used to create a new tab for the terminal window
// \\ must be escaped for it to work properly, or breaking it into multiple arguments
var addedCharacter = _settings.UseWindowsTerminal ? "\\" : "";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("pwsh");
}
else
{
info.FileName = "pwsh.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}{addedCharacter}; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'{addedCharacter}; [System.Console]::ReadKey(){addedCharacter}; exit" : "")}");
break;
}
else
{
info.FileName = "pwsh.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
break;
}
case Shell.RunCommand:
{
var parts = command.Split(new[]
{
' '
}, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ExistInPath(filename))
var parts = command.Split(new[]
{
var arguments = parts[1];
info.FileName = filename;
info.ArgumentList.Add(arguments);
' '
}, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ExistInPath(filename))
{
var arguments = parts[1];
info.FileName = filename;
info.ArgumentList.Add(arguments);
}
else
{
info.FileName = command;
}
}
else
{
info.FileName = command;
}
}
else
{
info.FileName = command;
info.UseShellExecute = true;
break;
}
info.UseShellExecute = true;
break;
}
default:
throw new NotImplementedException();
}