diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 895ec1ecd..0bd1a809b 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -166,6 +166,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool UseAnimation { get; set; } = true;
public bool UseSound { get; set; } = true;
public double SoundVolume { get; set; } = 50;
+ public bool ShowBadges { get; set; } = false;
+ public bool ShowBadgesGlobalOnly { get; set; } = false;
public bool UseClock { get; set; } = true;
public bool UseDate { get; set; } = false;
diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs
index 910485438..f561fcb1d 100644
--- a/Flow.Launcher.Plugin/Result.cs
+++ b/Flow.Launcher.Plugin/Result.cs
@@ -12,12 +12,19 @@ namespace Flow.Launcher.Plugin
///
public class Result
{
+ ///
+ /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
+ ///
+ public const int MaxScore = int.MaxValue;
+
private string _pluginDirectory;
private string _icoPath;
private string _copyText = string.Empty;
+ private string _badgeIcoPath;
+
///
/// The title of the result. This is always required.
///
@@ -60,7 +67,7 @@ namespace Flow.Launcher.Plugin
/// GlyphInfo is prioritized if not null
public string IcoPath
{
- get { return _icoPath; }
+ get => _icoPath;
set
{
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
@@ -80,6 +87,33 @@ namespace Flow.Launcher.Plugin
}
}
+ ///
+ /// The image to be displayed for the badge of the result.
+ ///
+ /// Can be a local file path or a URL.
+ /// If null or empty, will use plugin icon
+ public string BadgeIcoPath
+ {
+ get => _badgeIcoPath;
+ set
+ {
+ // As a standard this property will handle prepping and converting to absolute local path for icon image processing
+ if (!string.IsNullOrEmpty(value)
+ && !string.IsNullOrEmpty(PluginDirectory)
+ && !Path.IsPathRooted(value)
+ && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
+ && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
+ && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
+ {
+ _badgeIcoPath = Path.Combine(PluginDirectory, value);
+ }
+ else
+ {
+ _badgeIcoPath = value;
+ }
+ }
+ }
+
///
/// Determines if Icon has a border radius
///
@@ -94,14 +128,18 @@ namespace Flow.Launcher.Plugin
///
/// Delegate to load an icon for this result.
///
- public IconDelegate Icon;
+ public IconDelegate Icon { get; set; }
+
+ ///
+ /// Delegate to load an icon for the badge of this result.
+ ///
+ public IconDelegate BadgeIcon { get; set; }
///
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
///
public GlyphInfo Glyph { get; init; }
-
///
/// An action to take in the form of a function call when the result has been selected.
///
@@ -143,59 +181,19 @@ namespace Flow.Launcher.Plugin
///
public string PluginDirectory
{
- get { return _pluginDirectory; }
+ get => _pluginDirectory;
set
{
_pluginDirectory = value;
// When the Result object is returned from the query call, PluginDirectory is not provided until
// UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available
- // we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded.
+ // we need to update (only if not Uri path) the IcoPath and BadgeIcoPath with the full absolute path so the image can be loaded.
IcoPath = _icoPath;
+ BadgeIcoPath = _badgeIcoPath;
}
}
- ///
- public override string ToString()
- {
- return Title + SubTitle + Score;
- }
-
- ///
- /// Clones the current result
- ///
- public Result Clone()
- {
- return new Result
- {
- Title = Title,
- SubTitle = SubTitle,
- ActionKeywordAssigned = ActionKeywordAssigned,
- CopyText = CopyText,
- AutoCompleteText = AutoCompleteText,
- IcoPath = IcoPath,
- RoundedIcon = RoundedIcon,
- Icon = Icon,
- Glyph = Glyph,
- Action = Action,
- AsyncAction = AsyncAction,
- Score = Score,
- TitleHighlightData = TitleHighlightData,
- OriginQuery = OriginQuery,
- PluginDirectory = PluginDirectory,
- ContextData = ContextData,
- PluginID = PluginID,
- TitleToolTip = TitleToolTip,
- SubTitleToolTip = SubTitleToolTip,
- PreviewPanel = PreviewPanel,
- ProgressBar = ProgressBar,
- ProgressBarColor = ProgressBarColor,
- Preview = Preview,
- AddSelectedCount = AddSelectedCount,
- RecordKey = RecordKey
- };
- }
-
///
/// Additional data associated with this result
///
@@ -224,16 +222,6 @@ namespace Flow.Launcher.Plugin
///
public Lazy PreviewPanel { get; set; }
- ///
- /// Run this result, asynchronously
- ///
- ///
- ///
- public ValueTask ExecuteAsync(ActionContext context)
- {
- return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
- }
-
///
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
///
@@ -255,11 +243,6 @@ namespace Flow.Launcher.Plugin
///
public bool AddSelectedCount { get; set; } = true;
- ///
- /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
- ///
- public const int MaxScore = int.MaxValue;
-
///
/// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
/// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
@@ -268,6 +251,66 @@ namespace Flow.Launcher.Plugin
///
public string RecordKey { get; set; } = null;
+ ///
+ /// Determines if the badge icon should be shown.
+ /// If users want to show the result badges and here you set this to true, the results will show the badge icon.
+ ///
+ public bool ShowBadge { get; set; } = false;
+
+ ///
+ /// Run this result, asynchronously
+ ///
+ ///
+ ///
+ public ValueTask ExecuteAsync(ActionContext context)
+ {
+ return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return Title + SubTitle + Score;
+ }
+
+ ///
+ /// Clones the current result
+ ///
+ public Result Clone()
+ {
+ return new Result
+ {
+ Title = Title,
+ SubTitle = SubTitle,
+ ActionKeywordAssigned = ActionKeywordAssigned,
+ CopyText = CopyText,
+ AutoCompleteText = AutoCompleteText,
+ IcoPath = IcoPath,
+ BadgeIcoPath = BadgeIcoPath,
+ RoundedIcon = RoundedIcon,
+ Icon = Icon,
+ BadgeIcon = BadgeIcon,
+ Glyph = Glyph,
+ Action = Action,
+ AsyncAction = AsyncAction,
+ Score = Score,
+ TitleHighlightData = TitleHighlightData,
+ OriginQuery = OriginQuery,
+ PluginDirectory = PluginDirectory,
+ ContextData = ContextData,
+ PluginID = PluginID,
+ TitleToolTip = TitleToolTip,
+ SubTitleToolTip = SubTitleToolTip,
+ PreviewPanel = PreviewPanel,
+ ProgressBar = ProgressBar,
+ ProgressBarColor = ProgressBarColor,
+ Preview = Preview,
+ AddSelectedCount = AddSelectedCount,
+ RecordKey = RecordKey,
+ ShowBadge = ShowBadge,
+ };
+ }
+
///
/// Info of the preview section of a
///
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index 420da266d..9ec952155 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins
}
[SupportedOSPlatform("windows7.0")]
- [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
- [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
+ [TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
+ [TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
{
// Given
@@ -59,7 +59,7 @@ namespace Flow.Launcher.Test.Plugins
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
- " ORDER BY System.FileName")]
+ $" ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
string folderPath, string userSearchString, string expectedString)
{
@@ -87,8 +87,8 @@ namespace Flow.Launcher.Test.Plugins
[SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
- "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
- [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
+ $"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
+ [TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@@ -107,7 +107,6 @@ namespace Flow.Launcher.Test.Plugins
ClassicAssert.AreEqual(expectedString, resultString);
}
-
[SupportedOSPlatform("windows7.0")]
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
@@ -127,7 +126,7 @@ namespace Flow.Launcher.Test.Plugins
[SupportedOSPlatform("windows7.0")]
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
- "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
+ $"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
diff --git a/Flow.Launcher/Converters/BadgePositionConverter.cs b/Flow.Launcher/Converters/BadgePositionConverter.cs
new file mode 100644
index 000000000..66a7446f2
--- /dev/null
+++ b/Flow.Launcher/Converters/BadgePositionConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace Flow.Launcher.Converters;
+
+public class BadgePositionConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is double actualWidth && parameter is string param)
+ {
+ double offset = actualWidth / 2 - 8;
+
+ if (param == "1") // X-Offset
+ {
+ return offset + 2;
+ }
+ else if (param == "2") // Y-Offset
+ {
+ return offset + 2;
+ }
+ }
+
+ return 0.0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/Flow.Launcher/Converters/SizeRatioConverter.cs b/Flow.Launcher/Converters/SizeRatioConverter.cs
new file mode 100644
index 000000000..e61eeaf9b
--- /dev/null
+++ b/Flow.Launcher/Converters/SizeRatioConverter.cs
@@ -0,0 +1,27 @@
+using System.Windows.Data;
+using System;
+using System.Globalization;
+using System.Windows;
+
+namespace Flow.Launcher.Converters;
+
+public class SizeRatioConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is double size && parameter is string ratioString)
+ {
+ if (double.TryParse(ratioString, NumberStyles.Any, CultureInfo.InvariantCulture, out double ratio))
+ {
+ return size * ratio;
+ }
+ }
+
+ return 0.0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index f3a645767..5a6fd3d74 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -302,6 +302,10 @@
Use Segoe Fluent Icons
Use Segoe Fluent Icons for query results where supported
Press Key
+ Show Result Badges
+ Show badges for query results where supported
+ Show Result Badges for Global Query Only
+ Show badges for global query results only
HTTP Proxy
diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml
index 3744e0449..8cb15400f 100644
--- a/Flow.Launcher/ResultListBox.xaml
+++ b/Flow.Launcher/ResultListBox.xaml
@@ -32,6 +32,8 @@
+
+
@@ -90,60 +92,64 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index 4a6c1d639..f6b9aa67c 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -245,6 +245,14 @@ namespace Flow.Launcher.ViewModel
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
var resultsCopy = DeepCloneResults(e.Results, token);
+ foreach (var result in resultsCopy)
+ {
+ if (string.IsNullOrEmpty(result.BadgeIcoPath))
+ {
+ result.BadgeIcoPath = pair.Metadata.IcoPath;
+ }
+ }
+
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
@@ -1200,11 +1208,11 @@ namespace Flow.Launcher.ViewModel
_lastQuery = query;
- if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
+ if (string.IsNullOrEmpty(query.ActionKeyword))
{
- // Wait 45 millisecond for query change in global query
+ // Wait 15 millisecond for query change in global query
// if query changes, return so that it won't be calculated
- await Task.Delay(45, _updateSource.Token);
+ await Task.Delay(15, _updateSource.Token);
if (_updateSource.Token.IsCancellationRequested)
return;
}
@@ -1268,8 +1276,7 @@ namespace Flow.Launcher.ViewModel
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
- IReadOnlyList results =
- await PluginManager.QueryForPluginAsync(plugin, query, token);
+ var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
if (token.IsCancellationRequested)
return;
@@ -1285,6 +1292,14 @@ namespace Flow.Launcher.ViewModel
resultsCopy = DeepCloneResults(results, token);
}
+ foreach (var result in resultsCopy)
+ {
+ if (string.IsNullOrEmpty(result.BadgeIcoPath))
+ {
+ result.BadgeIcoPath = plugin.Metadata.IcoPath;
+ }
+ }
+
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
token, reSelect)))
{
diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs
index 9aab71a32..8d7569dc1 100644
--- a/Flow.Launcher/ViewModel/ResultViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Drawing.Text;
using System.IO;
@@ -108,7 +109,6 @@ namespace Flow.Launcher.ViewModel
return IconXY;
}
-
}
public Visibility ShowGlyph
@@ -124,10 +124,32 @@ namespace Flow.Launcher.ViewModel
}
}
+ public Visibility ShowBadge
+ {
+ get
+ {
+ // If results do not allow badges, or user has disabled badges in settings,
+ // or badge icon is not available, then do not show badge
+ if (!Result.ShowBadge || !Settings.ShowBadges || !BadgeIconAvailable)
+ return Visibility.Collapsed;
+
+ // If user has set to show badges only for global results, and this is not a global result,
+ // then do not show badge
+ if (Settings.ShowBadgesGlobalOnly && !IsGlobalQuery)
+ return Visibility.Collapsed;
+
+ return Visibility.Visible;
+ }
+ }
+
+ public bool IsGlobalQuery => string.IsNullOrEmpty(Result.OriginQuery.ActionKeyword);
+
private bool GlyphAvailable => Glyph is not null;
private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null;
+ private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null;
+
private bool PreviewImageAvailable => !string.IsNullOrEmpty(Result.Preview.PreviewImagePath) || Result.Preview.PreviewDelegate != null;
public string OpenResultModifiers => Settings.OpenResultModifiers;
@@ -141,9 +163,11 @@ namespace Flow.Launcher.ViewModel
: Result.SubTitleToolTip;
private volatile bool _imageLoaded;
+ private volatile bool _badgeImageLoaded;
private volatile bool _previewImageLoaded;
private ImageSource _image = ImageLoader.LoadingImage;
+ private ImageSource _badgeImage = ImageLoader.LoadingImage;
private ImageSource _previewImage = ImageLoader.LoadingImage;
public ImageSource Image
@@ -161,6 +185,21 @@ namespace Flow.Launcher.ViewModel
private set => _image = value;
}
+ public ImageSource BadgeImage
+ {
+ get
+ {
+ if (!_badgeImageLoaded)
+ {
+ _badgeImageLoaded = true;
+ _ = LoadBadgeImageAsync();
+ }
+
+ return _badgeImage;
+ }
+ private set => _badgeImage = value;
+ }
+
public ImageSource PreviewImage
{
get
@@ -206,7 +245,7 @@ namespace Flow.Launcher.ViewModel
{
var imagePath = Result.IcoPath;
var iconDelegate = Result.Icon;
- if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
+ if (ImageLoader.TryGetValue(imagePath, false, out var img))
{
_image = img;
}
@@ -217,11 +256,26 @@ namespace Flow.Launcher.ViewModel
}
}
+ private async Task LoadBadgeImageAsync()
+ {
+ var badgeImagePath = Result.BadgeIcoPath;
+ var badgeIconDelegate = Result.BadgeIcon;
+ if (ImageLoader.TryGetValue(badgeImagePath, false, out var img))
+ {
+ _badgeImage = img;
+ }
+ else
+ {
+ // We need to modify the property not field here to trigger the OnPropertyChanged event
+ BadgeImage = await LoadImageInternalAsync(badgeImagePath, badgeIconDelegate, false).ConfigureAwait(false);
+ }
+ }
+
private async Task LoadPreviewImageAsync()
{
var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath;
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
- if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
+ if (ImageLoader.TryGetValue(imagePath, true, out var img))
{
_previewImage = img;
}
diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs
index 61566b415..02fb379fa 100644
--- a/Flow.Launcher/ViewModel/ResultsViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs
@@ -19,7 +19,7 @@ namespace Flow.Launcher.ViewModel
public ResultCollection Results { get; }
- private readonly object _collectionLock = new object();
+ private readonly object _collectionLock = new();
private readonly Settings _settings;
private int MaxResults => _settings?.MaxResultsToShow ?? 6;
@@ -89,7 +89,7 @@ namespace Flow.Launcher.ViewModel
#region Private Methods
- private int InsertIndexOf(int newScore, IList list)
+ private static int InsertIndexOf(int newScore, IList list)
{
int index = 0;
for (; index < list.Count; index++)
@@ -118,7 +118,6 @@ namespace Flow.Launcher.ViewModel
}
}
-
#endregion
#region Public Methods
@@ -190,10 +189,10 @@ namespace Flow.Launcher.ViewModel
if (token.IsCancellationRequested)
return;
- UpdateResults(newResults, token, reselect);
+ UpdateResults(newResults, reselect, token);
}
- private void UpdateResults(List newResults, CancellationToken token = default, bool reselect = true)
+ private void UpdateResults(List newResults, bool reselect = true, CancellationToken token = default)
{
lock (_collectionLock)
{
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
index 9e3707fbe..1d160983a 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
@@ -6,16 +6,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class QueryConstructor
{
- private static Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
- private static Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
+ private static readonly Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
+ private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
- private Settings settings { get; }
+ private Settings Settings { get; }
private const string SystemIndex = "SystemIndex";
public QueryConstructor(Settings settings)
{
- this.settings = settings;
+ Settings = settings;
}
public CSearchQueryHelper CreateBaseQuery()
@@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
var baseQuery = CreateQueryHelper();
// Set the number of results we want. Don't set this property if all results are needed.
- baseQuery.QueryMaxResults = settings.MaxResult;
+ baseQuery.QueryMaxResults = Settings.MaxResult;
// Set list of columns we want to display, getting the path presently
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
@@ -37,7 +37,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return baseQuery;
}
- internal CSearchQueryHelper CreateQueryHelper()
+ internal static CSearchQueryHelper CreateQueryHelper()
{
// This uses the Microsoft.Search.Interop assembly
// Throws COMException if Windows Search service is not running/disabled, this needs to be caught
@@ -54,20 +54,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public static string TopLevelDirectoryConstraint(ReadOnlySpan path) => $"directory='file:{path}'";
public static string RecursiveDirectoryConstraint(ReadOnlySpan path) => $"scope='file:{path}'";
-
///
/// Search will be performed on all folders and files on the first level of a specified directory.
///
public string Directory(ReadOnlySpan path, ReadOnlySpan searchString = default, bool recursive = false)
{
- var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND ({FileName} LIKE '{searchString}%' OR CONTAINS({FileName},'\"{searchString}*\"'))";
+ var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
var scopeConstraint = recursive
? RecursiveDirectoryConstraint(path)
: TopLevelDirectoryConstraint(path);
- var query = $"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {FileName}";
+ var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
return query;
}
@@ -84,7 +83,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
- return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
+ return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
}
///
@@ -121,10 +120,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
///
- /// Order identifier: file name
+ /// Order identifier: System.Search.Rank DESC
///
- public const string FileName = "System.FileName";
-
+ ///
+ ///
+ ///
+ public const string OrderIdentifier = "System.Search.Rank DESC";
///
/// Search will be performed on all indexed file contents for the specified search keywords.
@@ -132,7 +133,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public string FileContent(ReadOnlySpan userSearchString)
{
string query =
- $"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
+ $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
return query;
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs
index c3a7d9e91..3d69a1ee6 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs
@@ -82,14 +82,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return HandledEngineNotAvailableExceptionAsync();
}
}
+
public IAsyncEnumerable SearchAsync(string search, CancellationToken token)
{
return WindowsIndexFilesAndFoldersSearchAsync(search, token: token);
}
+
public IAsyncEnumerable ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
{
return WindowsIndexFileContentSearchAsync(contentSearch, token);
}
+
public IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, CancellationToken token)
{
return WindowsIndexTopLevelFolderSearchAsync(search, path, recursive, token);
@@ -100,19 +103,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
if (!Settings.WarnWindowsSearchServiceOff)
return AsyncEnumerable.Empty();
- var api = Main.Context.API;
-
throw new EngineNotAvailableException(
"Windows Index",
- api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
- api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
+ Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
+ Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
Constants.WindowsIndexErrorImagePath,
c =>
{
Settings.WarnWindowsSearchServiceOff = false;
// Clears the warning message so user is not mistaken that it has not worked
- api.ChangeQuery(string.Empty);
+ Main.Context.API.ChangeQuery(string.Empty);
return ValueTask.FromResult(false);
});