diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index e304a1b5040..7c2457a72c7 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -101,6 +101,8 @@ public string Theme
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 9104854389d..f561fcb1dcf 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 @@ public string CopyText
/// 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 @@ public string IcoPath
}
}
+ ///
+ /// 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 @@ public string IcoPath
///
/// 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 @@ public string IcoPath
///
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 @@ public Result Clone()
///
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 @@ public ValueTask ExecuteAsync(ActionContext context)
///
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 @@ public ValueTask ExecuteAsync(ActionContext context)
///
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/Converters/BadgePositionConverter.cs b/Flow.Launcher/Converters/BadgePositionConverter.cs
new file mode 100644
index 00000000000..66a7446f2b6
--- /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 00000000000..e61eeaf9b3a
--- /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 d2549bd9152..9a9b8e1316e 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -298,6 +298,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 4c3bd1d120f..4141d9e2f1b 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 4a6c1d63971..f6b9aa67c94 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -245,6 +245,14 @@ public void RegisterResultsUpdatedEvent()
// 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 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b
_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 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token)
// 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 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token)
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 9aab71a328e..8d7569dc1e7 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 @@ public double IconRadius
return IconXY;
}
-
}
public Visibility ShowGlyph
@@ -124,10 +124,32 @@ public Visibility ShowGlyph
}
}
+ 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 @@ public Visibility ShowGlyph
: 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 @@ public ImageSource Image
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 @@ private async Task LoadImageAsync()
{
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 @@ private async Task LoadImageAsync()
}
}
+ 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 61566b41571..02fb379fa07 100644
--- a/Flow.Launcher/ViewModel/ResultsViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs
@@ -19,7 +19,7 @@ public class ResultsViewModel : BaseModel
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 @@ public double ItemHeightSize
#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 @@ private int NewIndex(int i)
}
}
-
#endregion
#region Public Methods
@@ -190,10 +189,10 @@ public void AddResults(ICollection resultsForUpdates, Cancella
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)
{