diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
index 649d59ad7..b5ba36f98 100644
--- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
+++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
@@ -72,9 +72,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs
index 2c9b8d4fd..9404d1107 100644
--- a/Flow.Launcher.Plugin/Result.cs
+++ b/Flow.Launcher.Plugin/Result.cs
@@ -4,11 +4,13 @@ using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
+using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
///
- /// Describes a result of a executed by a plugin
+ /// Describes a result of a executed by a plugin.
+ /// This or its child classes is serializable.
///
public class Result
{
@@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin
private string _icoPath;
+ private string _icoPathAbsolute;
+
private string _copyText = string.Empty;
private string _badgeIcoPath;
@@ -64,15 +68,27 @@ namespace Flow.Launcher.Plugin
public string AutoCompleteText { get; set; }
///
- /// The image to be displayed for the result.
+ /// Path or URI to the icon image for this result.
+ /// Updates appropriately when set.
///
- /// Can be a local file path or a URL.
- /// GlyphInfo is prioritized if not null
+ ///
+ /// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
+ /// Because is serialized, using relative paths keeps the icon reference portable
+ /// when Flow is moved.
+ ///
+ /// Accepted formats:
+ /// - Relative file paths (resolved against into )
+ /// - Absolute file paths (left as-is)
+ /// - HTTP/HTTPS URLs (left as-is)
+ /// - Data URIs (left as-is)
+ ///
public string IcoPath
{
get => _icoPath;
set
{
+ _icoPath = value;
+
// 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)
@@ -81,15 +97,23 @@ namespace Flow.Launcher.Plugin
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
- _icoPath = Path.Combine(PluginDirectory, value);
+ _icoPathAbsolute = Path.Combine(PluginDirectory, value);
}
else
{
- _icoPath = value;
+ _icoPathAbsolute = value;
}
}
}
+ ///
+ /// Absolute path or URI which is used to load and display the result icon for Flow.
+ /// This is populated by the setter.
+ /// If a relative path was provided to , this property will contain the resolved
+ /// absolute local path after combining with .
+ ///
+ public string IcoPathAbsolute => _icoPathAbsolute;
+
///
/// The image to be displayed for the badge of the result.
///
@@ -131,17 +155,34 @@ namespace Flow.Launcher.Plugin
///
/// Delegate to load an icon for this result.
///
+ [JsonIgnore]
public IconDelegate Icon = null;
///
/// Delegate to load an icon for the badge of this result.
///
+ [JsonIgnore]
public IconDelegate BadgeIcon = null;
+ private GlyphInfo _glyph;
+
///
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
///
- public GlyphInfo Glyph { get; init; }
+ public GlyphInfo Glyph
+ {
+ get => _glyph;
+ init => _glyph = value;
+ }
+
+ ///
+ /// Set the Glyph Icon after initialization
+ ///
+ ///
+ public void SetGlyph(GlyphInfo glyph)
+ {
+ _glyph = glyph;
+ }
///
/// An action to take in the form of a function call when the result has been selected.
@@ -151,6 +192,7 @@ namespace Flow.Launcher.Plugin
/// Its result determines what happens to Flow Launcher's query form:
/// when true, the form will be hidden; when false, it will stay in focus.
///
+ [JsonIgnore]
public Func Action { get; set; }
///
@@ -161,6 +203,7 @@ namespace Flow.Launcher.Plugin
/// Its result determines what happens to Flow Launcher's query form:
/// when true, the form will be hidden; when false, it will stay in focus.
///
+ [JsonIgnore]
public Func> AsyncAction { get; set; }
///
@@ -203,11 +246,13 @@ namespace Flow.Launcher.Plugin
///
/// As external information for ContextMenu
///
+ [JsonIgnore]
public object ContextData { get; set; }
///
/// Plugin ID that generated this result
///
+ [JsonInclude]
public string PluginID { get; internal set; }
///
@@ -223,6 +268,7 @@ namespace Flow.Launcher.Plugin
///
/// Customized Preview Panel
///
+ [JsonIgnore]
public Lazy PreviewPanel { get; set; }
///
@@ -352,6 +398,7 @@ namespace Flow.Launcher.Plugin
///
/// Delegate to get the preview panel's image
///
+ [JsonIgnore]
public IconDelegate PreviewDelegate { get; set; } = null;
///
diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json
index 70f71f20d..662192b4a 100644
--- a/Flow.Launcher.Plugin/packages.lock.json
+++ b/Flow.Launcher.Plugin/packages.lock.json
@@ -16,23 +16,23 @@
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
+ "requested": "[10.0.102, )",
+ "resolved": "10.0.102",
+ "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
+ "Microsoft.Build.Tasks.Git": "10.0.102",
+ "Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
- "requested": "[0.3.205, )",
- "resolved": "0.3.205",
- "contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==",
+ "requested": "[0.3.269, )",
+ "resolved": "0.3.269",
+ "contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
- "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview",
- "Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental"
+ "Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview",
+ "Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental"
}
},
"PropertyChanged.Fody": {
@@ -46,13 +46,13 @@
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
+ "resolved": "10.0.102",
+ "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
+ "resolved": "10.0.102",
+ "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
@@ -61,15 +61,15 @@
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
- "resolved": "61.0.15-preview",
- "contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ=="
+ "resolved": "69.0.7-preview",
+ "contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
- "resolved": "0.12.8-experimental",
- "contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==",
+ "resolved": "0.13.25-experimental",
+ "contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==",
"dependencies": {
- "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview"
+ "Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview"
}
}
}
diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs
index b075813db..4e40d3645 100644
--- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs
+++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs
@@ -16,14 +16,15 @@ namespace Flow.Launcher.Test.Plugins
{
DecimalSeparator = DecimalSeparator.UseSystemLocale,
MaxDecimalPlaces = 10,
- ShowErrorMessage = false // Make sure we return the empty results when error occurs
+ ShowErrorMessage = false, // Make sure we return the empty results when error occurs
+ UseThousandsSeparator = true // Default value
};
private readonly Engine _engine = new(new Configuration
{
Scope = new Dictionary
- {
- { "e", Math.E }, // e is not contained in the default mages engine
- }
+ {
+ { "e", Math.E }, // e is not contained in the default mages engine
+ }
});
public CalculatorPluginTest()
@@ -41,6 +42,44 @@ namespace Flow.Launcher.Test.Plugins
engineField.SetValue(null, _engine);
}
+ [Test]
+ public void ThousandsSeparatorTest_Enabled()
+ {
+ _settings.UseThousandsSeparator = true;
+
+ _settings.DecimalSeparator = DecimalSeparator.Dot;
+ var result = GetCalculationResult("1000+234");
+ // When thousands separator is enabled, the result should contain a separator
+ // Since decimal separator is dot, thousands separator should be comma
+ ClassicAssert.AreEqual("1,234", result);
+
+ _settings.DecimalSeparator = DecimalSeparator.Comma;
+ var result2 = GetCalculationResult("1000+234");
+ // When thousands separator is enabled, the result should contain a separator
+ // Since decimal separator is comma, thousands separator should be dot
+ ClassicAssert.AreEqual("1.234", result2);
+ }
+
+ [Test]
+ public void ThousandsSeparatorTest_Disabled()
+ {
+ _settings.UseThousandsSeparator = false;
+ _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
+
+ var result = GetCalculationResult("1000+234");
+ ClassicAssert.AreEqual("1234", result);
+ }
+
+ [Test]
+ public void ThousandsSeparatorTest_LargeNumber()
+ {
+ _settings.UseThousandsSeparator = false;
+ _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
+
+ var result = GetCalculationResult("1000000+234567");
+ ClassicAssert.AreEqual("1234567", result);
+ }
+
// Basic operations
[TestCase(@"1+1", "2")]
[TestCase(@"2-1", "1")]
@@ -77,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"invalid_expression", "")]
public void CalculatorTest(string expression, string result)
{
+ _settings.UseThousandsSeparator = false;
+ _settings.DecimalSeparator = DecimalSeparator.Dot;
+
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
}
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index b45bbc549..da11380b8 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -259,6 +259,9 @@ namespace Flow.Launcher
await PluginManager.InitializePluginsAsync(_mainVM);
+ // Refresh the history results after plugins are initialized so that we can parse the absolute icon paths
+ _mainVM.RefreshLastOpenedHistoryResults();
+
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
// And home page is created without full plugin list
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index f3c614702..576bf6f2f 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -138,7 +138,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs
index b8b7ff98e..017651fdf 100644
--- a/Flow.Launcher/Helper/ResultHelper.cs
+++ b/Flow.Launcher/Helper/ResultHelper.cs
@@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper;
public static class ResultHelper
{
- public static async Task PopulateResultsAsync(LastOpenedHistoryItem item)
+ public static async Task PopulateResultsAsync(LastOpenedHistoryResult item)
{
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
}
@@ -24,7 +24,7 @@ public static class ResultHelper
if (query == null) return null;
try
{
- var freshResults = await plugin.Plugin.QueryAsync(query, CancellationToken.None);
+ var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None);
// Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
if (string.IsNullOrEmpty(recordKey))
{
diff --git a/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
new file mode 100644
index 000000000..78985108c
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
@@ -0,0 +1,253 @@
+using iNKORE.UI.WPF.Modern.Controls;
+using iNKORE.UI.WPF.Modern.Controls.Helpers;
+using iNKORE.UI.WPF.Modern.Controls.Primitives;
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace Flow.Launcher.Resources.Controls
+{
+ // TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347
+ public class CustomScrollViewerEx : ScrollViewer
+ {
+ private double LastVerticalLocation = 0;
+ private double LastHorizontalLocation = 0;
+
+ public CustomScrollViewerEx()
+ {
+ Loaded += OnLoaded;
+ var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource;
+ if (valueSource == BaseValueSource.Default)
+ {
+ AutoPanningMode.SetIsEnabled(this, true);
+ }
+ }
+
+ #region Orientation
+
+ public static readonly DependencyProperty OrientationProperty =
+ DependencyProperty.Register(
+ nameof(Orientation),
+ typeof(Orientation),
+ typeof(CustomScrollViewerEx),
+ new PropertyMetadata(Orientation.Vertical));
+
+ public Orientation Orientation
+ {
+ get => (Orientation)GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ #endregion
+
+ #region AutoHideScrollBars
+
+ public static readonly DependencyProperty AutoHideScrollBarsProperty =
+ ScrollViewerHelper.AutoHideScrollBarsProperty
+ .AddOwner(
+ typeof(CustomScrollViewerEx),
+ new PropertyMetadata(true, OnAutoHideScrollBarsChanged));
+
+ public bool AutoHideScrollBars
+ {
+ get => (bool)GetValue(AutoHideScrollBarsProperty);
+ set => SetValue(AutoHideScrollBarsProperty, value);
+ }
+
+ private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is CustomScrollViewerEx sv)
+ {
+ sv.UpdateVisualState();
+ }
+ }
+
+ #endregion
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ LastVerticalLocation = VerticalOffset;
+ LastHorizontalLocation = HorizontalOffset;
+ UpdateVisualState(false);
+ }
+
+ ///
+ protected override void OnInitialized(EventArgs e)
+ {
+ base.OnInitialized(e);
+
+ if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue)
+ {
+ SetResourceReference(StyleProperty, typeof(ScrollViewer));
+ }
+ }
+
+ ///
+ protected override void OnMouseWheel(MouseWheelEventArgs e)
+ {
+ var Direction = GetDirection();
+ ScrollViewerBehavior.SetIsAnimating(this, true);
+
+ if (Direction == Orientation.Vertical)
+ {
+ if (ScrollableHeight > 0)
+ {
+ e.Handled = true;
+ }
+
+ var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight;
+ var newOffset = LastVerticalLocation - WheelChange;
+
+ if (newOffset < 0)
+ {
+ newOffset = 0;
+ }
+
+ if (newOffset > ScrollableHeight)
+ {
+ newOffset = ScrollableHeight;
+ }
+
+ if (newOffset == LastVerticalLocation)
+ {
+ return;
+ }
+
+ ScrollToVerticalOffset(LastVerticalLocation);
+
+ ScrollToValue(newOffset, Direction);
+ LastVerticalLocation = newOffset;
+ }
+ else
+ {
+ if (ScrollableWidth > 0)
+ {
+ e.Handled = true;
+ }
+
+ var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth;
+ var newOffset = LastHorizontalLocation - WheelChange;
+
+ if (newOffset < 0)
+ {
+ newOffset = 0;
+ }
+
+ if (newOffset > ScrollableWidth)
+ {
+ newOffset = ScrollableWidth;
+ }
+
+ if (newOffset == LastHorizontalLocation)
+ {
+ return;
+ }
+
+ ScrollToHorizontalOffset(LastHorizontalLocation);
+
+ ScrollToValue(newOffset, Direction);
+ LastHorizontalLocation = newOffset;
+ }
+ }
+
+ ///
+ protected override void OnScrollChanged(ScrollChangedEventArgs e)
+ {
+ base.OnScrollChanged(e);
+ if (!ScrollViewerBehavior.GetIsAnimating(this))
+ {
+ LastVerticalLocation = VerticalOffset;
+ LastHorizontalLocation = HorizontalOffset;
+ }
+ }
+
+ private Orientation GetDirection()
+ {
+ var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
+
+ if (Orientation == Orientation.Horizontal)
+ {
+ return isShiftDown ? Orientation.Vertical : Orientation.Horizontal;
+ }
+ else
+ {
+ return isShiftDown ? Orientation.Horizontal : Orientation.Vertical;
+ }
+ }
+
+ ///
+ /// Causes the to load a new view into the viewport using the specified offsets and zoom factor.
+ ///
+ /// A value between 0 and that specifies the distance the content should be scrolled horizontally.
+ /// A value between 0 and that specifies the distance the content should be scrolled vertically.
+ /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.
+ /// if the view is changed; otherwise, .
+ public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor)
+ {
+ return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false);
+ }
+
+ ///
+ /// Causes the to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation.
+ ///
+ /// A value between 0 and that specifies the distance the content should be scrolled horizontally.
+ /// A value between 0 and that specifies the distance the content should be scrolled vertically.
+ /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.
+ /// to disable zoom/pan animations while changing the view; otherwise, . The default is false.
+ /// if the view is changed; otherwise, .
+ public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation)
+ {
+ if (disableAnimation)
+ {
+ if (horizontalOffset.HasValue)
+ {
+ ScrollToHorizontalOffset(horizontalOffset.Value);
+ }
+
+ if (verticalOffset.HasValue)
+ {
+ ScrollToVerticalOffset(verticalOffset.Value);
+ }
+ }
+ else
+ {
+ if (horizontalOffset.HasValue)
+ {
+ ScrollToHorizontalOffset(LastHorizontalLocation);
+ ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal);
+ LastHorizontalLocation = horizontalOffset.Value;
+ }
+
+ if (verticalOffset.HasValue)
+ {
+ ScrollToVerticalOffset(LastVerticalLocation);
+ ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical);
+ LastVerticalLocation = verticalOffset.Value;
+ }
+ }
+
+ return true;
+ }
+
+ private void ScrollToValue(double value, Orientation Direction)
+ {
+ if (Direction == Orientation.Vertical)
+ {
+ ScrollToVerticalOffset(value);
+ }
+ else
+ {
+ ScrollToHorizontalOffset(value);
+ }
+
+ ScrollViewerBehavior.SetIsAnimating(this, false);
+ }
+
+ private void UpdateVisualState(bool useTransitions = true)
+ {
+ var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator";
+ VisualStateManager.GoToState(this, stateName, useTransitions);
+ }
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml
index aa027e19e..4d37dc93a 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml
@@ -333,7 +333,7 @@
Margin="18 24 0 0"
HorizontalAlignment="Left"
RenderOptions.BitmapScalingMode="Fant"
- Source="{Binding IcoPath, IsAsync=True}" />
+ Source="{Binding IcoPathAbsolute, IsAsync=True}" />
+/// A serializable result used to record the last opened history for reopening results.
+/// Inherits common result fields from and adds the original query and execution time.
+///
+public class LastOpenedHistoryResult : Result
+{
+ ///
+ /// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class .
+ /// This is used so results can be reopened or re-run using the serialized query string.
+ ///
+ public string Query { get; set; } = string.Empty;
+
+ ///
+ /// The local date and time when this result was executed/opened.
+ ///
+ public DateTime ExecutedDateTime { get; set; }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public LastOpenedHistoryResult()
+ {
+ }
+
+ ///
+ /// Creates a from an existing .
+ /// Copies required fields and sets up default reopening actions.
+ ///
+ /// The original result to create history from.
+ public LastOpenedHistoryResult(Result result)
+ {
+ Title = result.Title;
+ SubTitle = result.SubTitle;
+ PluginID = result.PluginID;
+ Query = result.OriginQuery.TrimmedQuery;
+ OriginQuery = result.OriginQuery;
+ RecordKey = result.RecordKey;
+ IcoPath = result.IcoPath;
+ PluginDirectory = result.PluginDirectory;
+ Glyph = result.Glyph;
+ ExecutedDateTime = DateTime.Now;
+ // Used for Query History style reopening
+ Action = _ =>
+ {
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(result.OriginQuery.TrimmedQuery);
+ return false;
+ };
+ // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs
+ AsyncAction = null;
+ }
+
+ ///
+ /// Selectively creates a deep copy of the required properties for
+ /// based on the style of history- Last Opened or Query.
+ /// This copy should be independent of original and full isolated.
+ ///
+ /// A new containing the same required data.
+ public LastOpenedHistoryResult DeepCopyForHistoryStyle(bool isHistoryStyleLastOpened)
+ {
+ // queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate.
+ var queryValue = Query;
+ var glyphValue = Glyph;
+
+ var title = string.Empty;
+ var showBadge = false;
+ var badgeIcoPath = string.Empty;
+ var icoPath = string.Empty;
+ var glyph = null as GlyphInfo;
+
+ if (isHistoryStyleLastOpened)
+ {
+ title = Title;
+ icoPath = IcoPath;
+ glyph = glyphValue != null
+ ? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph)
+ : null;
+ showBadge = true;
+ badgeIcoPath = Constant.HistoryIcon;
+ }
+ else
+ {
+ title = Localize.executeQuery(Query);
+ icoPath = Constant.HistoryIcon;
+ glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C");
+ showBadge = false;
+ }
+
+ return new LastOpenedHistoryResult
+ {
+ Title = title,
+ // Subtitle has datetime which can cause duplicates when saving.
+ SubTitle = Localize.lastExecuteTime(ExecutedDateTime),
+ // Empty PluginID so the source of last opened history results won't be updated, this copy is meant to be temporary.
+ PluginID = string.Empty,
+ Query = Query,
+ OriginQuery = new Query { TrimmedQuery = Query },
+ RecordKey = RecordKey,
+ IcoPath = icoPath,
+ ShowBadge = showBadge,
+ BadgeIcoPath = badgeIcoPath,
+ PluginDirectory = PluginDirectory,
+ // Used for Query History style reopening
+ Action = _ =>
+ {
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(queryValue);
+ return false;
+ },
+ // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs
+ AsyncAction = null,
+ Glyph = glyph,
+ ExecutedDateTime = ExecutedDateTime
+ // Note: Other properties are left as default — copy if needed.
+ };
+ }
+
+ ///
+ /// Determines whether the specified is equivalent to this history result.
+ /// Comparison uses when available; otherwise falls back to title/subtitle/plugin id and query.
+ ///
+ /// The result to compare to.
+ /// true if the results are considered equal; otherwise false.
+ public bool Equals(Result r)
+ {
+ if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey))
+ {
+ return Title == r.Title
+ && SubTitle == r.SubTitle
+ && PluginID == r.PluginID
+ && Query == r.OriginQuery.TrimmedQuery;
+ }
+ else
+ {
+ return RecordKey == r.RecordKey
+ && PluginID == r.PluginID
+ && Query == r.OriginQuery.TrimmedQuery;
+ }
+ }
+}
diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs
index 6998a4ae3..7bf948399 100644
--- a/Flow.Launcher/Storage/QueryHistory.cs
+++ b/Flow.Launcher/Storage/QueryHistory.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Storage
@@ -14,28 +15,50 @@ namespace Flow.Launcher.Storage
#pragma warning restore CS0618 // Type or member is obsolete
[JsonInclude]
- public List LastOpenedHistoryItems { get; private set; } = [];
+ public List LastOpenedHistoryItems { get; private set; } = [];
private readonly int _maxHistory = 300;
+ ///
+ /// Migrate legacy history data (stored in ) into the new
+ /// format and append them to
+ /// .
+ ///
+ [Obsolete("For backwards compatibility. Remove after release v2.3.0")]
public void PopulateHistoryFromLegacyHistory()
{
if (Items.Count == 0) return;
// Migrate old history items to new LastOpenedHistoryItems
foreach (var item in Items)
{
- LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
+ LastOpenedHistoryItems.Add(new LastOpenedHistoryResult
{
+ Title = Localize.executeQuery(item.Query),
+ OriginQuery = new Query { TrimmedQuery = item.Query },
Query = item.Query,
+ Action = _ =>
+ {
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(item.Query);
+ return false;
+ },
ExecutedDateTime = item.ExecutedDateTime
});
}
Items.Clear();
}
+ ///
+ /// Records a result into the last-opened history list ().
+ /// This will also update the IcoPath if existing history item has one that is different.
+ ///
+ /// The result to add to history. Must have a non-empty ..
public void Add(Result result)
{
if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return;
+ // History results triggered from homepage do not contain PluginID,
+ // these are intentionally not saved otherwise cause duplicates due to subtitle
+ // containing datetime string.
if (string.IsNullOrEmpty(result.PluginID)) return;
// Maintain the max history limit
@@ -44,23 +67,53 @@ namespace Flow.Launcher.Storage
LastOpenedHistoryItems.RemoveAt(0);
}
- // If the last item is the same as the current result, just update the timestamp
- if (LastOpenedHistoryItems.Count > 0 &&
- LastOpenedHistoryItems.Last().Equals(result))
+ // If the last item is the same as the current result, just update the timestamp and the icon path
+ if (LastOpenedHistoryItems.Count > 0 &&
+ TryGetLastOpenedHistoryResult(result, out var existingHistoryItem))
{
- LastOpenedHistoryItems.Last().ExecutedDateTime = DateTime.Now;
+ existingHistoryItem.ExecutedDateTime = DateTime.Now;
+
+ if (existingHistoryItem.IcoPath != result.IcoPath)
+ existingHistoryItem.IcoPath = result.IcoPath;
+
+ if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph
+ || existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily)
+ existingHistoryItem.SetGlyph(result.Glyph);
}
else
{
- LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
- {
- Title = result.Title,
- SubTitle = result.SubTitle,
- PluginID = result.PluginID,
- Query = result.OriginQuery.TrimmedQuery,
- RecordKey = result.RecordKey,
- ExecutedDateTime = DateTime.Now
- });
+ LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result));
+ }
+ }
+
+ ///
+ /// Attempts to find an existing in
+ /// that is considered equal to the supplied .
+ ///
+ private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem)
+ {
+ historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result));
+ return historyItem is not null;
+ }
+
+ ///
+ /// Flow uses IcoPathAbsolute property to display result the icons. This refreshes the IcoPathAbsolute
+ /// property using current plugin metadata by updating the PluginDirectory property, which in turn also
+ /// updates IcoPath. This keeps the saved icon paths of results updated correctly if flow is moved around.
+ ///
+ /// Call this after plugins are loaded/initialized.
+ public void UpdateIcoPathAbsolute()
+ {
+ if (LastOpenedHistoryItems.Count == 0) return;
+
+ foreach (var item in LastOpenedHistoryItems)
+ {
+ if (string.IsNullOrEmpty(item.PluginID)) continue;
+
+ var pluginPair = PluginManager.GetPluginForId(item.PluginID);
+ if (pluginPair == null) continue;
+
+ item.PluginDirectory = pluginPair.Metadata.PluginDirectory;
}
}
}
diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml
index c3831e68f..c5b45890b 100644
--- a/Flow.Launcher/Themes/Base.xaml
+++ b/Flow.Launcher/Themes/Base.xaml
@@ -252,14 +252,12 @@
-
-
-
-
+
-
+
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index a28b2b33e..333ac3652 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
@@ -37,16 +38,16 @@ namespace Flow.Launcher.ViewModel
private Query _lastQuery;
private bool _previousIsHomeQuery;
- private Query _progressQuery; // Used for QueryResultAsync
+ private readonly ConcurrentDictionary _progressQueryDict = new(); // Used for QueryResultAsync
private Query _updateQuery; // Used for ResultsUpdated
private string _queryTextBeforeLeaveResults;
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results
private readonly FlowLauncherJsonStorage _historyItemsStorage;
- private readonly FlowLauncherJsonStorage _userSelectedRecordStorage;
- private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
private readonly History _history;
private int lastHistoryIndex = 1;
+ private readonly FlowLauncherJsonStorage _userSelectedRecordStorage;
+ private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
private readonly UserSelectedRecord _userSelectedRecord;
private CancellationTokenSource _updateSource; // Used to cancel old query flows
@@ -152,11 +153,10 @@ namespace Flow.Launcher.ViewModel
};
_historyItemsStorage = new FlowLauncherJsonStorage();
- _userSelectedRecordStorage = new FlowLauncherJsonStorage();
- _topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
_history = _historyItemsStorage.Load();
- _history.PopulateHistoryFromLegacyHistory();
+ _userSelectedRecordStorage = new FlowLauncherJsonStorage();
_userSelectedRecord = _userSelectedRecordStorage.Load();
+ _topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
ContextMenu = new ResultsViewModel(Settings, this)
{
@@ -355,11 +355,17 @@ namespace Flow.Launcher.ViewModel
if (QueryResultsSelected())
{
SelectedResults = History;
- History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1;
+ if (History.Results.Count > 0)
+ {
+ SelectedResults.SelectedIndex = 0;
+ SelectedResults.SelectedItem = History.Results[0];
+ }
}
else
{
SelectedResults = Results;
+ PreviewSelectedItem = Results.SelectedItem;
+ _ = UpdatePreviewAsync();
}
}
@@ -431,7 +437,8 @@ namespace Flow.Launcher.ViewModel
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
- if (SelectedResults.SelectedItem != null)
+ if (SelectedResults.SelectedItem?.Result != null &&
+ !string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history results
{
SelectedResults = ContextMenu;
}
@@ -439,6 +446,8 @@ namespace Flow.Launcher.ViewModel
else
{
SelectedResults = Results;
+ PreviewSelectedItem = Results.SelectedItem;
+ _ = UpdatePreviewAsync();
}
}
@@ -642,6 +651,8 @@ namespace Flow.Launcher.ViewModel
if (!QueryResultsSelected())
{
SelectedResults = Results;
+ PreviewSelectedItem = Results.SelectedItem;
+ _ = UpdatePreviewAsync();
}
else
{
@@ -1252,22 +1263,12 @@ namespace Flow.Launcher.ViewModel
var selected = Results.SelectedItem?.Result;
- if (selected != null) // SelectedItem returns null if selection is empty.
+ if (selected != null && // SelectedItem returns null if selection is empty.
+ !string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID, history results do not.
{
- List results;
- if (selected.PluginID == null) // SelectedItem from history in home page.
- {
- results = new()
- {
- ContextMenuTopMost(selected)
- };
- }
- else
- {
- results = PluginManager.GetContextMenusForPlugin(selected);
- results.Add(ContextMenuTopMost(selected));
- results.Add(ContextMenuPluginInfo(selected));
- }
+ List results = PluginManager.GetContextMenusForPlugin(selected);
+ results.Add(ContextMenuTopMost(selected));
+ results.Add(ContextMenuPluginInfo(selected));
if (!string.IsNullOrEmpty(query))
{
@@ -1318,68 +1319,77 @@ namespace Flow.Launcher.ViewModel
}
}
- private List GetHistoryItems(IEnumerable historyItems)
+ private List GetHistoryItems(IEnumerable historyItems, int? maxResult = null)
{
var results = new List();
- if (Settings.HistoryStyle == HistoryStyle.Query)
- {
- foreach (var h in historyItems)
- {
- var result = new Result
- {
- Title = Localize.executeQuery(h.Query),
- SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
- IcoPath = Constant.HistoryIcon,
- OriginQuery = new Query { TrimmedQuery = h.Query },
- Action = _ =>
- {
- App.API.BackToQueryResults();
- App.API.ChangeQuery(h.Query);
- return false;
- },
- Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
- };
- results.Add(result);
- }
- }
- else
- {
- foreach (var h in historyItems)
- {
- var result = new Result
- {
- Title = string.IsNullOrEmpty(h.Title) ? // Old migrated history items have no title
- Localize.executeQuery(h.Query) :
- h.Title,
- SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
- IcoPath = Constant.HistoryIcon,
- OriginQuery = new Query { TrimmedQuery = h.Query },
- AsyncAction = async c =>
- {
- var reflectResult = await ResultHelper.PopulateResultsAsync(h);
- if (reflectResult != null)
- {
- // Record the user selected record for result ranking
- _userSelectedRecord.Add(reflectResult);
- // Since some actions may need to hide the Flow window to execute
- // So let us populate the results of them
- return await reflectResult.ExecuteAsync(c);
- }
+ // Order by executed time descending: Latest -> Oldest
+ historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime);
- // If we cannot get the result, fallback to re-query
- App.API.BackToQueryResults();
- App.API.ChangeQuery(h.Query);
- return false;
- },
- Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
- };
- results.Add(result);
- }
+ if (Settings.HistoryStyle == HistoryStyle.LastOpened)
+ {
+ // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results
+ historyItems = historyItems
+ .GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey })
+ .Select(g => g.First());
}
+
+ // Max history results to return for display
+ if (maxResult.HasValue)
+ {
+ historyItems = historyItems.Take(maxResult.Value);
+ }
+
+ foreach (var item in historyItems)
+ {
+ var copiedItem = item.DeepCopyForHistoryStyle(Settings.HistoryStyle == HistoryStyle.LastOpened);
+
+ if (Settings.HistoryStyle == HistoryStyle.LastOpened)
+ {
+ copiedItem.AsyncAction = async c =>
+ {
+ // Use original history item to reflect correct result because properties like subtitle have been modified in copiedItem
+ var reflectResult = await ResultHelper.PopulateResultsAsync(item);
+ if (reflectResult != null)
+ {
+ // Since some actions may need to hide the Flow window to execute
+ // So let us populate the results of them
+ return await reflectResult.ExecuteAsync(c);
+ }
+
+ // If we cannot get the result, fallback to re-query
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(copiedItem.Query);
+ return false;
+ };
+ }
+
+ results.Add(copiedItem);
+ }
+
return results;
}
+ ///
+ /// Refreshes the last-opened history storage by migrating legacy entries and
+ /// updating stored icon paths to their resolved (absolute) locations.
+ ///
+ ///
+ /// Calls to refresh absolute icon
+ /// paths on the migrated/saved history entries by updating each item's
+ /// PluginDirectory (which in turn resolves IcoPathAbsolute).
+ ///
+ /// Important:
+ /// - Plugins must be initialized (their metadata and PluginDirectory set)
+ /// before calling this method; otherwise icon resolution cannot be performed.
+ ///
+ internal void RefreshLastOpenedHistoryResults()
+ {
+ _history.PopulateHistoryFromLegacyHistory();
+
+ _history.UpdateIcoPathAbsolute();
+ }
+
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{
_updateSource?.Cancel();
@@ -1406,6 +1416,9 @@ namespace Flow.Launcher.ViewModel
return;
}
+ // Create a Guid for this update session so that we can filter out in progress checking
+ var updateGuid = Guid.NewGuid();
+
try
{
_updateSource?.Dispose();
@@ -1417,7 +1430,7 @@ namespace Flow.Launcher.ViewModel
ProgressBarVisibility = Visibility.Hidden;
- _progressQuery = query;
+ _progressQueryDict.TryAdd(updateGuid, query);
_updateQuery = query;
// Switch to ThreadPool thread
@@ -1472,7 +1485,8 @@ namespace Flow.Launcher.ViewModel
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
{
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
- if (_progressQuery != null && _progressQuery.OriginalQuery == query.OriginalQuery)
+ if (_progressQueryDict.TryGetValue(updateGuid, out var progressQuery) &&
+ progressQuery.OriginalQuery == query.OriginalQuery)
{
ProgressBarVisibility = Visibility.Visible;
}
@@ -1528,7 +1542,7 @@ namespace Flow.Launcher.ViewModel
// this should happen once after all queries are done so progress bar should continue
// until the end of all querying
- _progressQuery = null;
+ _progressQueryDict.Remove(updateGuid, out _);
if (!currentCancellationToken.IsCancellationRequested)
{
@@ -1538,8 +1552,8 @@ namespace Flow.Launcher.ViewModel
}
finally
{
- // this make sures progress query is null when this query is canceled
- _progressQuery = null;
+ // this ensures the query is removed from the progress tracking dictionary when this query is canceled or completes
+ _progressQueryDict.Remove(updateGuid, out _);
}
// Local function
@@ -1617,10 +1631,8 @@ namespace Flow.Launcher.ViewModel
void QueryHistoryTask(CancellationToken token)
{
- // Select last history results and revert its order to make sure last history results are on top
- var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
-
- var results = GetHistoryItems(historyItems);
+ // Select last history results
+ var results = GetHistoryItems(_history.LastOpenedHistoryItems, Settings.MaxHistoryResultsToShowForHomePage);
if (token.IsCancellationRequested) return;
diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs
index f2f49f8f1..077acfce7 100644
--- a/Flow.Launcher/ViewModel/ResultViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultViewModel.cs
@@ -141,7 +141,7 @@ namespace Flow.Launcher.ViewModel
private bool GlyphAvailable => Glyph is not null;
- private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null;
+ private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPathAbsolute) || Result.Icon is not null;
private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null;
@@ -236,7 +236,7 @@ namespace Flow.Launcher.ViewModel
private async Task LoadImageAsync()
{
- var imagePath = Result.IcoPath;
+ var imagePath = Result.IcoPathAbsolute;
var iconDelegate = Result.Icon;
if (ImageLoader.TryGetValue(imagePath, false, out var img))
{
@@ -266,7 +266,7 @@ namespace Flow.Launcher.ViewModel
private async Task LoadPreviewImageAsync()
{
- var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath;
+ var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPathAbsolute;
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
if (ImageLoader.TryGetValue(imagePath, true, out var img))
{
diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json
index 8c3a16e5e..b4b929d19 100644
--- a/Flow.Launcher/packages.lock.json
+++ b/Flow.Launcher/packages.lock.json
@@ -28,9 +28,9 @@
},
"iNKORE.UI.WPF.Modern": {
"type": "Direct",
- "requested": "[0.10.2.1, )",
- "resolved": "0.10.2.1",
- "contentHash": "nGwuuVul+TcLCTgPmaAZCc0fYFqUpCNZ8PiulVT3gZnsWt/AvxMZ0DSPpuyI/iRPc/NhFIg9lSIR7uaHWV0I/Q==",
+ "requested": "[0.10.1, )",
+ "resolved": "0.10.1",
+ "contentHash": "nRYmBosiL+42eUpLbHeqP7qJqtp5EpzuIMZTpvq4mFV33VB/JjkFg1y82gk50pjkXlAQWDvRyrfSAmPR5AM+3g==",
"dependencies": {
"iNKORE.UI.WPF": "1.2.8"
}
@@ -1619,7 +1619,7 @@
"FSharp.Core": "[9.0.303, )",
"Flow.Launcher.Infrastructure": "[1.0.0, )",
"Flow.Launcher.Localization": "[0.0.6, )",
- "Flow.Launcher.Plugin": "[5.1.0, )",
+ "Flow.Launcher.Plugin": "[5.0.0, )",
"Meziantou.Framework.Win32.Jobs": "[3.4.5, )",
"Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )",
"SemanticVersioning": "[3.0.0, )",
@@ -1634,7 +1634,7 @@
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Localization": "[0.0.6, )",
- "Flow.Launcher.Plugin": "[5.1.0, )",
+ "Flow.Launcher.Plugin": "[5.0.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index ed121375b..cb86f719e 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -105,9 +105,9 @@
-
-
-
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
index b12972b1b..5f9a3ca5a 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
@@ -14,6 +14,7 @@
Comma (,)
Dot (.)
Max. decimal places
+ Show thousands separator in results
Copy failed, please try later
Show error message when calculation fails
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
index a20a1ad5d..1b9a38e18 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
@@ -363,7 +363,7 @@ namespace Flow.Launcher.Plugin.Calculator
string integerPart = parts[0];
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
- if (integerPart.Length > 3)
+ if (_settings.UseThousandsSeparator && integerPart.Length > 3)
{
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
}
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs
index 1544dc41f..69cd17ed2 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs
@@ -7,4 +7,6 @@ public class Settings
public int MaxDecimalPlaces { get; set; } = 10;
public bool ShowErrorMessage { get; set; } = false;
+
+ public bool UseThousandsSeparator { get; set; } = true;
}
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml
index 9e7549b2d..82f8eec60 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml
@@ -16,6 +16,7 @@
+
@@ -66,6 +67,16 @@
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
+ Content="{DynamicResource flowlauncher_plugin_calculator_use_thousands_separator}"
+ IsChecked="{Binding Settings.UseThousandsSeparator, Mode=TwoWay}" />
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
index 956c84db2..30c2c7f14 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
@@ -532,7 +532,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
[RelayCommand]
private void OpenShellPath()
{
- var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null);
+ var path = PromptUserSelectPath(ResultType.File, Settings.ShellPath != null ? Path.GetDirectoryName(Settings.ShellPath) : null);
if (path is null)
return;
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
index 39586771f..ec8a32b95 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
@@ -54,7 +54,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
index 4cf09baab..1ef6dc495 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
@@ -60,7 +60,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
index 57b9749f7..e0da6cbe1 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
@@ -175,7 +175,7 @@ namespace Flow.Launcher.Plugin.Sys
Privileges = new() { e0 = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED } }
};
- if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, 0, null, null))
+ if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, null, out var _))
{
return false;
}
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj
index 42176376b..3b3add106 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj
@@ -50,6 +50,10 @@
+
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml
index 5d65e4462..2b2b06fae 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml
@@ -21,6 +21,7 @@
URL
Search
Use Search Query Autocomplete
+ Max Suggestions
Autocomplete Data from:
Please select a web search
Are you sure you want to delete {0}?
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs
index b9b8a0b19..299e27499 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -91,6 +91,7 @@ namespace Flow.Launcher.Plugin.WebSearch
if (token.IsCancellationRequested)
return null;
+
}
return results;
@@ -126,7 +127,7 @@ namespace Flow.Launcher.Plugin.WebSearch
token.ThrowIfCancellationRequested();
- var resultsFromSuggestion = suggestions?.Select(o => new Result
+ var resultsFromSuggestion = suggestions?.Take(_settings.MaxSuggestions).Select(o => new Result
{
Title = o,
SubTitle = subtitle,
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs
index 0c0ac4b84..31540ad92 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs
@@ -205,6 +205,23 @@ namespace Flow.Launcher.Plugin.WebSearch
}
}
+ private int maxSuggestions = 1;
+ public int MaxSuggestions
+ {
+ get => maxSuggestions;
+ set
+ {
+ if (value > 0 && value <= 1000)
+ {
+ if (maxSuggestions != value)
+ {
+ maxSuggestions = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+ }
+
[JsonIgnore]
public SuggestionSource[] Suggestions { get; set; } = {
new Google(),
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml
index 152558e81..e4f3485cd 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml
@@ -3,7 +3,9 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:vm="clr-namespace:Flow.Launcher.Plugin.WebSearch"
d:DataContext="{d:DesignInstance vm:SettingsViewModel}"
d:DesignHeight="300"
@@ -45,17 +47,17 @@
x:Name="SearchSourcesListView"
Grid.Row="0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
+ AllowDrop="True"
BorderBrush="DarkGray"
BorderThickness="1"
+ Drop="ListView_Drop"
GridViewColumnHeader.Click="SortByColumn"
ItemsSource="{Binding Settings.SearchSources}"
MouseDoubleClick="MouseDoubleClickItem"
- SelectedItem="{Binding Settings.SelectedSearchSource}"
- SizeChanged="ListView_SizeChanged"
PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown"
PreviewMouseMove="ListView_PreviewMouseMove"
- AllowDrop="True"
- Drop="ListView_Drop"
+ SelectedItem="{Binding Settings.SelectedSearchSource}"
+ SizeChanged="ListView_SizeChanged"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
@@ -146,31 +148,43 @@
-
-
-
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
index 6dc766fff..1c1dd3116 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs
@@ -240,5 +240,15 @@ namespace Flow.Launcher.Plugin.WebSearch
}
return null;
}
+
+ // This is used for NumberBox to force its value to be 1 when the user clears the value
+ private void NumberBox_ValueChanged(iNKORE.UI.WPF.Modern.Controls.NumberBox sender, iNKORE.UI.WPF.Modern.Controls.NumberBoxValueChangedEventArgs args)
+ {
+ if (double.IsNaN(args.NewValue))
+ {
+ sender.Value = 1;
+ _settings.MaxSuggestions = (int)sender.Value;
+ }
+ }
}
}