Skip to content

Commit 2e740b1

Browse files
authored
Merge pull request #3361 from onesounds/250320BookmarkFavicon
Support Svg File Icon Loading & Local favicon for bookmark plugin
2 parents c730361 + 2d29a42 commit 2e740b1

34 files changed

+646
-244
lines changed

Flow.Launcher.Core/Resource/LocalizationConverter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Flow.Launcher.Core.Resource
88
{
9+
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
910
public class LocalizationConverter : IValueConverter
1011
{
1112
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
using System.ComponentModel;
2+
using CommunityToolkit.Mvvm.DependencyInjection;
3+
using Flow.Launcher.Plugin;
24

35
namespace Flow.Launcher.Core.Resource
46
{
57
public class LocalizedDescriptionAttribute : DescriptionAttribute
68
{
7-
private readonly Internationalization _translator;
9+
// We should not initialize API in static constructor because it will create another API instance
10+
private static IPublicAPI api = null;
11+
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
12+
813
private readonly string _resourceKey;
914

1015
public LocalizedDescriptionAttribute(string resourceKey)
1116
{
12-
_translator = InternationalizationManager.Instance;
1317
_resourceKey = resourceKey;
1418
}
1519

1620
public override string Description
1721
{
1822
get
1923
{
20-
string description = _translator.GetTranslation(_resourceKey);
24+
string description = API.GetTranslation(_resourceKey);
2125
return string.IsNullOrWhiteSpace(description) ?
2226
string.Format("[[{0}]]", _resourceKey) : description;
2327
}
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
using System;
22
using System.Globalization;
33
using System.Windows.Data;
4+
using CommunityToolkit.Mvvm.DependencyInjection;
5+
using Flow.Launcher.Plugin;
46

57
namespace Flow.Launcher.Core.Resource
68
{
79
public class TranslationConverter : IValueConverter
810
{
11+
// We should not initialize API in static constructor because it will create another API instance
12+
private static IPublicAPI api = null;
13+
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
14+
915
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
1016
{
1117
var key = value.ToString();
12-
if (String.IsNullOrEmpty(key))
13-
return key;
14-
return InternationalizationManager.Instance.GetTranslation(key);
18+
if (string.IsNullOrEmpty(key)) return key;
19+
return API.GetTranslation(key);
1520
}
1621

17-
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
22+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
23+
throw new InvalidOperationException();
1824
}
1925
}

Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
</PackageReference>
6868
<PackageReference Include="NLog" Version="4.7.10" />
6969
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
70+
<PackageReference Include="SharpVectors.Wpf" Version="1.8.4.2" />
7071
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
7172
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
7273
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->

Flow.Launcher.Infrastructure/Image/ImageCache.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.Linq;
5-
using System.Threading;
64
using System.Threading.Tasks;
75
using System.Windows.Media;
86
using BitFaster.Caching.Lfu;
@@ -55,7 +53,6 @@ public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
5553
return image != null;
5654
}
5755

58-
5956
image = null;
6057
return false;
6158
}

Flow.Launcher.Infrastructure/Image/ImageLoader.cs

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using CommunityToolkit.Mvvm.DependencyInjection;
1111
using Flow.Launcher.Infrastructure.Storage;
1212
using Flow.Launcher.Plugin;
13+
using SharpVectors.Converters;
14+
using SharpVectors.Renderers.Wpf;
1315

1416
namespace Flow.Launcher.Infrastructure.Image
1517
{
@@ -32,8 +34,10 @@ public static class ImageLoader
3234
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
3335
public const int SmallIconSize = 64;
3436
public const int FullIconSize = 256;
37+
public const int FullImageSize = 320;
3538

3639
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
40+
private static readonly string SvgExtension = ".svg";
3741

3842
public static async Task InitializeAsync()
3943
{
@@ -235,10 +239,11 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag
235239
image = LoadFullImage(path);
236240
type = ImageType.FullImageFile;
237241
}
238-
catch (NotSupportedException)
242+
catch (NotSupportedException ex)
239243
{
240244
image = Image;
241245
type = ImageType.Error;
246+
API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
242247
}
243248
}
244249
else
@@ -251,6 +256,20 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag
251256
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
252257
}
253258
}
259+
else if (extension == SvgExtension)
260+
{
261+
try
262+
{
263+
image = LoadSvgImage(path, loadFullImage);
264+
type = ImageType.FullImageFile;
265+
}
266+
catch (System.Exception ex)
267+
{
268+
image = Image;
269+
type = ImageType.Error;
270+
API.LogException(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
271+
}
272+
}
254273
else
255274
{
256275
type = ImageType.File;
@@ -324,7 +343,7 @@ public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullI
324343
return img;
325344
}
326345

327-
private static BitmapImage LoadFullImage(string path)
346+
private static ImageSource LoadFullImage(string path)
328347
{
329348
BitmapImage image = new BitmapImage();
330349
image.BeginInit();
@@ -333,24 +352,24 @@ private static BitmapImage LoadFullImage(string path)
333352
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
334353
image.EndInit();
335354

336-
if (image.PixelWidth > 320)
355+
if (image.PixelWidth > FullImageSize)
337356
{
338357
BitmapImage resizedWidth = new BitmapImage();
339358
resizedWidth.BeginInit();
340359
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
341360
resizedWidth.UriSource = new Uri(path);
342361
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
343-
resizedWidth.DecodePixelWidth = 320;
362+
resizedWidth.DecodePixelWidth = FullImageSize;
344363
resizedWidth.EndInit();
345364

346-
if (resizedWidth.PixelHeight > 320)
365+
if (resizedWidth.PixelHeight > FullImageSize)
347366
{
348367
BitmapImage resizedHeight = new BitmapImage();
349368
resizedHeight.BeginInit();
350369
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
351370
resizedHeight.UriSource = new Uri(path);
352371
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
353-
resizedHeight.DecodePixelHeight = 320;
372+
resizedHeight.DecodePixelHeight = FullImageSize;
354373
resizedHeight.EndInit();
355374
return resizedHeight;
356375
}
@@ -360,5 +379,50 @@ private static BitmapImage LoadFullImage(string path)
360379

361380
return image;
362381
}
382+
383+
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
384+
{
385+
// Set up drawing settings
386+
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;
387+
var drawingSettings = new WpfDrawingSettings
388+
{
389+
IncludeRuntime = true,
390+
// Set IgnoreRootViewbox to false to respect the SVG's viewBox
391+
IgnoreRootViewbox = false
392+
};
393+
394+
// Load and render the SVG
395+
var converter = new FileSvgReader(drawingSettings);
396+
var drawing = converter.Read(new Uri(path));
397+
398+
// Calculate scale to achieve desired height
399+
var drawingBounds = drawing.Bounds;
400+
if (drawingBounds.Height <= 0)
401+
{
402+
throw new InvalidOperationException($"Invalid SVG dimensions: Height must be greater than zero in {path}");
403+
}
404+
var scale = desiredHeight / drawingBounds.Height;
405+
var scaledWidth = drawingBounds.Width * scale;
406+
var scaledHeight = drawingBounds.Height * scale;
407+
408+
// Convert the Drawing to a Bitmap
409+
var drawingVisual = new DrawingVisual();
410+
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
411+
{
412+
drawingContext.PushTransform(new ScaleTransform(scale, scale));
413+
drawingContext.DrawDrawing(drawing);
414+
}
415+
416+
// Create a RenderTargetBitmap to hold the rendered image
417+
var bitmap = new RenderTargetBitmap(
418+
(int)Math.Ceiling(scaledWidth),
419+
(int)Math.Ceiling(scaledHeight),
420+
96, // DpiX
421+
96, // DpiY
422+
PixelFormats.Pbgra32);
423+
bitmap.Render(drawingVisual);
424+
425+
return bitmap;
426+
}
363427
}
364428
}

Flow.Launcher.Infrastructure/NativeMethods.txt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ GetModuleHandle
1111
GetKeyState
1212
VIRTUAL_KEY
1313

14-
WM_KEYDOWN
15-
WM_KEYUP
16-
WM_SYSKEYDOWN
17-
WM_SYSKEYUP
18-
1914
EnumWindows
2015

2116
DwmSetWindowAttribute

Flow.Launcher.Infrastructure/UI/EnumBindingSource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace Flow.Launcher.Infrastructure.UI
55
{
6+
[Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
67
public class EnumBindingSourceExtension : MarkupExtension
78
{
89
private Type _enumType;

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,47 @@ public interface IPublicAPI
142142
List<PluginPair> GetAllPlugins();
143143

144144
/// <summary>
145-
/// Register a callback for Global Keyboard Event
146-
/// </summary>
147-
/// <param name="callback"></param>
145+
/// Registers a callback function for global keyboard events.
146+
/// </summary>
147+
/// <param name="callback">
148+
/// The callback function to invoke when a global keyboard event occurs.
149+
/// <para>
150+
/// Parameters:
151+
/// <list type="number">
152+
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
153+
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
154+
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
155+
/// </list>
156+
/// </para>
157+
/// <para>
158+
/// Returns: <c>true</c> to allow normal system processing of the key event,
159+
/// or <c>false</c> to intercept and prevent default handling.
160+
/// </para>
161+
/// </param>
162+
/// <remarks>
163+
/// This callback will be invoked for all keyboard events system-wide.
164+
/// Use with caution as intercepting system keys may affect normal system operation.
165+
/// </remarks>
148166
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
149-
167+
150168
/// <summary>
151169
/// Remove a callback for Global Keyboard Event
152170
/// </summary>
153-
/// <param name="callback"></param>
171+
/// <param name="callback">
172+
/// The callback function to invoke when a global keyboard event occurs.
173+
/// <para>
174+
/// Parameters:
175+
/// <list type="number">
176+
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
177+
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
178+
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
179+
/// </list>
180+
/// </para>
181+
/// <para>
182+
/// Returns: <c>true</c> to allow normal system processing of the key event,
183+
/// or <c>false</c> to intercept and prevent default handling.
184+
/// </para>
185+
/// </param>
154186
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
155187

156188
/// <summary>
@@ -372,6 +404,7 @@ public interface IPublicAPI
372404
/// </returns>
373405
public bool SetCurrentTheme(ThemeData theme);
374406

407+
/// <summary>
375408
/// Save all Flow's plugins caches
376409
/// </summary>
377410
void SavePluginCaches();
@@ -404,7 +437,10 @@ public interface IPublicAPI
404437
/// </remarks>
405438
Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new();
406439

407-
/// Load image from path. Support local, remote and data:image url.
440+
/// <summary>
441+
/// Load image from path.
442+
/// Support local, remote and data:image url.
443+
/// Support png, jpg, jpeg, gif, bmp, tiff, ico, svg image files.
408444
/// If image path is missing, it will return a missing icon.
409445
/// </summary>
410446
/// <param name="path">The path of the image.</param>
@@ -418,6 +454,7 @@ public interface IPublicAPI
418454
/// <returns></returns>
419455
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
420456

457+
/// <summary>
421458
/// Update the plugin manifest
422459
/// </summary>
423460
/// <param name="usePrimaryUrlOnly">

Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs renamed to Flow.Launcher.Plugin/KeyEvent.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
using Windows.Win32;
22

3-
namespace Flow.Launcher.Infrastructure.Hotkey
3+
namespace Flow.Launcher.Plugin
44
{
5+
/// <summary>
6+
/// Enumeration of key events for
7+
/// <see cref="IPublicAPI.RegisterGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
8+
/// and <see cref="IPublicAPI.RemoveGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
9+
/// </summary>
510
public enum KeyEvent
611
{
712
/// <summary>
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
EnumThreadWindows
22
GetWindowText
3-
GetWindowTextLength
3+
GetWindowTextLength
4+
5+
WM_KEYDOWN
6+
WM_KEYUP
7+
WM_SYSKEYDOWN
8+
WM_SYSKEYUP

Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private async Task RefreshExternalPluginsAsync()
3232
public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
3333
{
3434
return string.IsNullOrEmpty(FilterText) ||
35-
StringMatcher.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
36-
StringMatcher.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
35+
App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
36+
App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
3737
}
3838
}

Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ public SettingsPanePluginsViewModel(Settings settings)
106106
public List<PluginViewModel> FilteredPluginViewModels => PluginViewModels
107107
.Where(v =>
108108
string.IsNullOrEmpty(FilterText) ||
109-
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
110-
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
109+
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
110+
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
111111
)
112112
.ToList();
113113

0 commit comments

Comments
 (0)