Merge pull request #3361 from onesounds/250320BookmarkFavicon

Support Svg File Icon Loading & Local favicon for bookmark plugin
This commit is contained in:
DB P 2025-04-09 18:56:25 +09:00 committed by GitHub
commit 2e740b15ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 642 additions and 240 deletions

View file

@ -6,6 +6,7 @@ using System.Windows.Data;
namespace Flow.Launcher.Core.Resource namespace Flow.Launcher.Core.Resource
{ {
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
public class LocalizationConverter : IValueConverter public class LocalizationConverter : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

View file

@ -1,15 +1,19 @@
using System.ComponentModel; using System.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource namespace Flow.Launcher.Core.Resource
{ {
public class LocalizedDescriptionAttribute : DescriptionAttribute public class LocalizedDescriptionAttribute : DescriptionAttribute
{ {
private readonly Internationalization _translator; // We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private readonly string _resourceKey; private readonly string _resourceKey;
public LocalizedDescriptionAttribute(string resourceKey) public LocalizedDescriptionAttribute(string resourceKey)
{ {
_translator = InternationalizationManager.Instance;
_resourceKey = resourceKey; _resourceKey = resourceKey;
} }
@ -17,7 +21,7 @@ namespace Flow.Launcher.Core.Resource
{ {
get get
{ {
string description = _translator.GetTranslation(_resourceKey); string description = API.GetTranslation(_resourceKey);
return string.IsNullOrWhiteSpace(description) ? return string.IsNullOrWhiteSpace(description) ?
string.Format("[[{0}]]", _resourceKey) : description; string.Format("[[{0}]]", _resourceKey) : description;
} }

View file

@ -1,19 +1,25 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Windows.Data; using System.Windows.Data;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource namespace Flow.Launcher.Core.Resource
{ {
public class TranslationConverter : IValueConverter public class TranslationConverter : IValueConverter
{ {
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
var key = value.ToString(); var key = value.ToString();
if (String.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key)) return key;
return key; return API.GetTranslation(key);
return InternationalizationManager.Instance.GetTranslation(key);
} }
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new InvalidOperationException();
} }
} }

View file

@ -67,6 +67,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="NLog" Version="4.7.10" /> <PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" /> <PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SharpVectors.Wpf" Version="1.8.4.2" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" /> <PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled--> <!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
<!--Bumping to it or higher needs to test and ensure this is no longer a problem--> <!--Bumping to it or higher needs to test and ensure this is no longer a problem-->

View file

@ -1,8 +1,6 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
using BitFaster.Caching.Lfu; using BitFaster.Caching.Lfu;
@ -55,7 +53,6 @@ namespace Flow.Launcher.Infrastructure.Image
return image != null; return image != null;
} }
image = null; image = null;
return false; return false;
} }

View file

@ -10,6 +10,8 @@ using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;
namespace Flow.Launcher.Infrastructure.Image namespace Flow.Launcher.Infrastructure.Image
{ {
@ -32,8 +34,10 @@ namespace Flow.Launcher.Infrastructure.Image
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon)); public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
public const int SmallIconSize = 64; public const int SmallIconSize = 64;
public const int FullIconSize = 256; public const int FullIconSize = 256;
public const int FullImageSize = 320;
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
private static readonly string SvgExtension = ".svg";
public static async Task InitializeAsync() public static async Task InitializeAsync()
{ {
@ -235,10 +239,11 @@ namespace Flow.Launcher.Infrastructure.Image
image = LoadFullImage(path); image = LoadFullImage(path);
type = ImageType.FullImageFile; type = ImageType.FullImageFile;
} }
catch (NotSupportedException) catch (NotSupportedException ex)
{ {
image = Image; image = Image;
type = ImageType.Error; type = ImageType.Error;
API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
} }
} }
else else
@ -251,6 +256,20 @@ namespace Flow.Launcher.Infrastructure.Image
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly); image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
} }
} }
else if (extension == SvgExtension)
{
try
{
image = LoadSvgImage(path, loadFullImage);
type = ImageType.FullImageFile;
}
catch (System.Exception ex)
{
image = Image;
type = ImageType.Error;
API.LogException(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
}
}
else else
{ {
type = ImageType.File; type = ImageType.File;
@ -324,7 +343,7 @@ namespace Flow.Launcher.Infrastructure.Image
return img; return img;
} }
private static BitmapImage LoadFullImage(string path) private static ImageSource LoadFullImage(string path)
{ {
BitmapImage image = new BitmapImage(); BitmapImage image = new BitmapImage();
image.BeginInit(); image.BeginInit();
@ -333,24 +352,24 @@ namespace Flow.Launcher.Infrastructure.Image
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit(); image.EndInit();
if (image.PixelWidth > 320) if (image.PixelWidth > FullImageSize)
{ {
BitmapImage resizedWidth = new BitmapImage(); BitmapImage resizedWidth = new BitmapImage();
resizedWidth.BeginInit(); resizedWidth.BeginInit();
resizedWidth.CacheOption = BitmapCacheOption.OnLoad; resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
resizedWidth.UriSource = new Uri(path); resizedWidth.UriSource = new Uri(path);
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedWidth.DecodePixelWidth = 320; resizedWidth.DecodePixelWidth = FullImageSize;
resizedWidth.EndInit(); resizedWidth.EndInit();
if (resizedWidth.PixelHeight > 320) if (resizedWidth.PixelHeight > FullImageSize)
{ {
BitmapImage resizedHeight = new BitmapImage(); BitmapImage resizedHeight = new BitmapImage();
resizedHeight.BeginInit(); resizedHeight.BeginInit();
resizedHeight.CacheOption = BitmapCacheOption.OnLoad; resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
resizedHeight.UriSource = new Uri(path); resizedHeight.UriSource = new Uri(path);
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedHeight.DecodePixelHeight = 320; resizedHeight.DecodePixelHeight = FullImageSize;
resizedHeight.EndInit(); resizedHeight.EndInit();
return resizedHeight; return resizedHeight;
} }
@ -360,5 +379,50 @@ namespace Flow.Launcher.Infrastructure.Image
return image; return image;
} }
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
{
// Set up drawing settings
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;
var drawingSettings = new WpfDrawingSettings
{
IncludeRuntime = true,
// Set IgnoreRootViewbox to false to respect the SVG's viewBox
IgnoreRootViewbox = false
};
// Load and render the SVG
var converter = new FileSvgReader(drawingSettings);
var drawing = converter.Read(new Uri(path));
// Calculate scale to achieve desired height
var drawingBounds = drawing.Bounds;
if (drawingBounds.Height <= 0)
{
throw new InvalidOperationException($"Invalid SVG dimensions: Height must be greater than zero in {path}");
}
var scale = desiredHeight / drawingBounds.Height;
var scaledWidth = drawingBounds.Width * scale;
var scaledHeight = drawingBounds.Height * scale;
// Convert the Drawing to a Bitmap
var drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.PushTransform(new ScaleTransform(scale, scale));
drawingContext.DrawDrawing(drawing);
}
// Create a RenderTargetBitmap to hold the rendered image
var bitmap = new RenderTargetBitmap(
(int)Math.Ceiling(scaledWidth),
(int)Math.Ceiling(scaledHeight),
96, // DpiX
96, // DpiY
PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
return bitmap;
}
} }
} }

View file

@ -11,11 +11,6 @@ GetModuleHandle
GetKeyState GetKeyState
VIRTUAL_KEY VIRTUAL_KEY
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
EnumWindows EnumWindows
DwmSetWindowAttribute DwmSetWindowAttribute

View file

@ -3,6 +3,7 @@ using System.Windows.Markup;
namespace Flow.Launcher.Infrastructure.UI namespace Flow.Launcher.Infrastructure.UI
{ {
[Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
public class EnumBindingSourceExtension : MarkupExtension public class EnumBindingSourceExtension : MarkupExtension
{ {
private Type _enumType; private Type _enumType;

View file

@ -142,15 +142,47 @@ namespace Flow.Launcher.Plugin
List<PluginPair> GetAllPlugins(); List<PluginPair> GetAllPlugins();
/// <summary> /// <summary>
/// Register a callback for Global Keyboard Event /// Registers a callback function for global keyboard events.
/// </summary> /// </summary>
/// <param name="callback"></param> /// <param name="callback">
/// The callback function to invoke when a global keyboard event occurs.
/// <para>
/// Parameters:
/// <list type="number">
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
/// </list>
/// </para>
/// <para>
/// Returns: <c>true</c> to allow normal system processing of the key event,
/// or <c>false</c> to intercept and prevent default handling.
/// </para>
/// </param>
/// <remarks>
/// This callback will be invoked for all keyboard events system-wide.
/// Use with caution as intercepting system keys may affect normal system operation.
/// </remarks>
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback); public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
/// <summary> /// <summary>
/// Remove a callback for Global Keyboard Event /// Remove a callback for Global Keyboard Event
/// </summary> /// </summary>
/// <param name="callback"></param> /// <param name="callback">
/// The callback function to invoke when a global keyboard event occurs.
/// <para>
/// Parameters:
/// <list type="number">
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
/// </list>
/// </para>
/// <para>
/// Returns: <c>true</c> to allow normal system processing of the key event,
/// or <c>false</c> to intercept and prevent default handling.
/// </para>
/// </param>
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback); public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
/// <summary> /// <summary>
@ -372,6 +404,7 @@ namespace Flow.Launcher.Plugin
/// </returns> /// </returns>
public bool SetCurrentTheme(ThemeData theme); public bool SetCurrentTheme(ThemeData theme);
/// <summary>
/// Save all Flow's plugins caches /// Save all Flow's plugins caches
/// </summary> /// </summary>
void SavePluginCaches(); void SavePluginCaches();
@ -404,7 +437,10 @@ namespace Flow.Launcher.Plugin
/// </remarks> /// </remarks>
Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new(); Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new();
/// Load image from path. Support local, remote and data:image url. /// <summary>
/// Load image from path.
/// Support local, remote and data:image url.
/// Support png, jpg, jpeg, gif, bmp, tiff, ico, svg image files.
/// If image path is missing, it will return a missing icon. /// If image path is missing, it will return a missing icon.
/// </summary> /// </summary>
/// <param name="path">The path of the image.</param> /// <param name="path">The path of the image.</param>
@ -418,6 +454,7 @@ namespace Flow.Launcher.Plugin
/// <returns></returns> /// <returns></returns>
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true); ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
/// <summary>
/// Update the plugin manifest /// Update the plugin manifest
/// </summary> /// </summary>
/// <param name="usePrimaryUrlOnly"> /// <param name="usePrimaryUrlOnly">

View file

@ -1,7 +1,12 @@
using Windows.Win32; using Windows.Win32;
namespace Flow.Launcher.Infrastructure.Hotkey namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// Enumeration of key events for
/// <see cref="IPublicAPI.RegisterGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
/// and <see cref="IPublicAPI.RemoveGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
/// </summary>
public enum KeyEvent public enum KeyEvent
{ {
/// <summary> /// <summary>

View file

@ -1,3 +1,8 @@
EnumThreadWindows EnumThreadWindows
GetWindowText GetWindowText
GetWindowTextLength GetWindowTextLength
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP

View file

@ -32,7 +32,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
public bool SatisfiesFilter(PluginStoreItemViewModel plugin) public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
{ {
return string.IsNullOrEmpty(FilterText) || return string.IsNullOrEmpty(FilterText) ||
StringMatcher.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() || App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
StringMatcher.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet(); App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
} }
} }

View file

@ -106,8 +106,8 @@ public partial class SettingsPanePluginsViewModel : BaseModel
public List<PluginViewModel> FilteredPluginViewModels => PluginViewModels public List<PluginViewModel> FilteredPluginViewModels => PluginViewModels
.Where(v => .Where(v =>
string.IsNullOrEmpty(FilterText) || string.IsNullOrEmpty(FilterText) ||
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet() App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
) )
.ToList(); .ToList();

View file

@ -1,13 +1,23 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models; using System.Collections.Generic;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using Flow.Launcher.Infrastructure.Logger; using System;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
namespace Flow.Launcher.Plugin.BrowserBookmark; namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class ChromiumBookmarkLoader : IBookmarkLoader public abstract class ChromiumBookmarkLoader : IBookmarkLoader
{ {
private static readonly string ClassName = nameof(ChromiumBookmarkLoader);
private readonly string _faviconCacheDir;
protected ChromiumBookmarkLoader()
{
_faviconCacheDir = Main._faviconCacheDir;
}
public abstract List<Bookmark> GetBookmarks(); public abstract List<Bookmark> GetBookmarks();
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name) protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
@ -22,16 +32,36 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
if (!File.Exists(bookmarkPath)) if (!File.Exists(bookmarkPath))
continue; continue;
Main.RegisterBookmarkFile(bookmarkPath); // Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile)
try
{
if (File.Exists(bookmarkPath))
{
Main.RegisterBookmarkFile(bookmarkPath);
}
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
}
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source)); var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
// Load favicons after loading bookmarks
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
}
bookmarks.AddRange(profileBookmarks);
} }
return bookmarks; return bookmarks;
} }
protected List<Bookmark> LoadBookmarksFromFile(string path, string source) protected static List<Bookmark> LoadBookmarksFromFile(string path, string source)
{ {
var bookmarks = new List<Bookmark>(); var bookmarks = new List<Bookmark>();
@ -45,15 +75,14 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
return bookmarks; return bookmarks;
} }
private void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source) private static void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source)
{ {
foreach (var folder in rootElement.EnumerateObject()) foreach (var folder in rootElement.EnumerateObject())
{ {
if (folder.Value.ValueKind != JsonValueKind.Object) if (folder.Value.ValueKind != JsonValueKind.Object)
continue; continue;
// Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details. // Fix for Opera. It stores bookmarks slightly different than chrome.
// If various exceptions start to build up here consider splitting this Loader into multiple separate ones.
if (folder.Name == "custom_root") if (folder.Name == "custom_root")
EnumerateRoot(folder.Value, bookmarks, source); EnumerateRoot(folder.Value, bookmarks, source);
else else
@ -61,7 +90,7 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
} }
} }
private void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks, private static void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks,
string source) string source)
{ {
if (!folderElement.TryGetProperty("children", out var childrenElement)) if (!folderElement.TryGetProperty("children", out var childrenElement))
@ -86,9 +115,107 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
} }
else else
{ {
Log.Error( Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString()}");
$"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}");
} }
} }
} }
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
{
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db");
try
{
File.Copy(dbPath, tempDbPath, true);
}
catch (Exception ex)
{
File.Delete(tempDbPath);
Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex);
return;
}
try
{
using var connection = new SqliteConnection($"Data Source={tempDbPath}");
connection.Open();
foreach (var bookmark in bookmarks)
{
try
{
var url = bookmark.Url;
if (string.IsNullOrEmpty(url)) continue;
// Extract domain from URL
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
continue;
var domain = uri.Host;
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT f.id, b.image_data
FROM favicons f
JOIN favicon_bitmaps b ON f.id = b.icon_id
JOIN icon_mapping m ON f.id = m.icon_id
WHERE m.page_url LIKE @url
ORDER BY b.width DESC
LIMIT 1";
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
using var reader = cmd.ExecuteReader();
if (!reader.Read() || reader.IsDBNull(1))
continue;
var iconId = reader.GetInt64(0).ToString();
var imageData = (byte[])reader["image_data"];
if (imageData is not { Length: > 0 })
continue;
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
SaveBitmapData(imageData, faviconPath);
bookmark.FaviconPath = faviconPath;
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
}
}
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to connect to SQLite: {tempDbPath}", ex);
}
// Delete temporary file
try
{
File.Delete(tempDbPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex);
}
}
private static void SaveBitmapData(byte[] imageData, string outputPath)
{
try
{
File.WriteAllBytes(outputPath, imageData);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex);
}
}
} }

View file

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
@ -10,11 +9,11 @@ internal static class BookmarkLoader
{ {
internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) internal static MatchResult MatchProgram(Bookmark bookmark, string queryString)
{ {
var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); var match = Main._context.API.FuzzySearch(queryString, bookmark.Name);
if (match.IsSearchPrecisionScoreMet()) if (match.IsSearchPrecisionScoreMet())
return match; return match;
return StringMatcher.FuzzySearch(queryString, bookmark.Url); return Main._context.API.FuzzySearch(queryString, bookmark.Url);
} }
internal static List<Bookmark> LoadAllBookmarks(Settings setting) internal static List<Bookmark> LoadAllBookmarks(Settings setting)

View file

@ -1,16 +1,26 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models; using System;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
namespace Flow.Launcher.Plugin.BrowserBookmark; namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
{ {
private static readonly string ClassName = nameof(FirefoxBookmarkLoaderBase);
private readonly string _faviconCacheDir;
protected FirefoxBookmarkLoaderBase()
{
_faviconCacheDir = Main._faviconCacheDir;
}
public abstract List<Bookmark> GetBookmarks(); public abstract List<Bookmark> GetBookmarks();
// Updated query - removed favicon_id column
private const string QueryAllBookmarks = """ private const string QueryAllBookmarks = """
SELECT moz_places.url, moz_bookmarks.title SELECT moz_places.url, moz_bookmarks.title
FROM moz_places FROM moz_places
@ -20,36 +30,194 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
ORDER BY moz_places.visit_count DESC ORDER BY moz_places.visit_count DESC
"""; """;
private const string DbPathFormat = "Data Source ={0}"; private const string DbPathFormat = "Data Source={0}";
protected static List<Bookmark> GetBookmarksFromPath(string placesPath) protected List<Bookmark> GetBookmarksFromPath(string placesPath)
{ {
// Return empty list if the places.sqlite file cannot be found // Variable to store bookmark list
var bookmarks = new List<Bookmark>();
// Return empty list if places.sqlite file doesn't exist
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath)) if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
return new List<Bookmark>(); return bookmarks;
Main.RegisterBookmarkFile(placesPath); var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite");
// create the connection string and init the connection try
string dbPath = string.Format(DbPathFormat, placesPath); {
using var dbConnection = new SqliteConnection(dbPath); // Try to register file monitoring
// Open connection to the database file and execute the query try
dbConnection.Open(); {
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader(); Main.RegisterBookmarkFile(placesPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
}
// return results in List<Bookmark> format // Use a copy to avoid lock issues with the original file
return reader File.Copy(placesPath, tempDbPath, true);
.Select(
x => new Bookmark( // Connect to database and execute query
x["title"] is DBNull ? string.Empty : x["title"].ToString(), string dbPath = string.Format(DbPathFormat, tempDbPath);
x["url"].ToString() using var dbConnection = new SqliteConnection(dbPath);
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
// Create bookmark list
bookmarks = reader
.Select(
x => new Bookmark(
x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString(),
"Firefox"
)
) )
) .ToList();
.ToList();
// Path to favicon database
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
if (File.Exists(faviconDbPath))
{
LoadFaviconsFromDb(faviconDbPath, bookmarks);
}
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(dbConnection);
dbConnection.Close();
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to load Firefox bookmarks: {placesPath}", ex);
}
// Delete temporary file
try
{
File.Delete(tempDbPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex);
}
return bookmarks;
}
private void LoadFaviconsFromDb(string faviconDbPath, List<Bookmark> bookmarks)
{
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.sqlite");
try
{
// Use a copy to avoid lock issues with the original file
File.Copy(faviconDbPath, tempDbPath, true);
var defaultIconPath = Path.Combine(
Path.GetDirectoryName(typeof(FirefoxBookmarkLoaderBase).Assembly.Location),
"bookmark.png");
string dbPath = string.Format(DbPathFormat, tempDbPath);
using var connection = new SqliteConnection(dbPath);
connection.Open();
// Get favicons based on bookmark URLs
foreach (var bookmark in bookmarks)
{
try
{
if (string.IsNullOrEmpty(bookmark.Url))
continue;
// Extract domain from URL
if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri))
continue;
var domain = uri.Host;
// Query for latest Firefox version favicon structure
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT i.data
FROM moz_icons i
JOIN moz_icons_to_pages ip ON i.id = ip.icon_id
JOIN moz_pages_w_icons p ON ip.page_id = p.id
WHERE p.page_url LIKE @url
AND i.data IS NOT NULL
ORDER BY i.width DESC -- Select largest icon available
LIMIT 1";
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
using var reader = cmd.ExecuteReader();
if (!reader.Read() || reader.IsDBNull(0))
continue;
var imageData = (byte[])reader["data"];
if (imageData is not { Length: > 0 })
continue;
string faviconPath;
if (IsSvgData(imageData))
{
faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.svg");
}
else
{
faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png");
}
SaveBitmapData(imageData, faviconPath);
bookmark.FaviconPath = faviconPath;
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to extract Firefox favicon: {bookmark.Url}", ex);
}
}
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {faviconDbPath}", ex);
}
// Delete temporary file
try
{
File.Delete(tempDbPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex);
}
}
private static void SaveBitmapData(byte[] imageData, string outputPath)
{
try
{
File.WriteAllBytes(outputPath, imageData);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex);
}
}
private static bool IsSvgData(byte[] data)
{
if (data.Length < 5)
return false;
string start = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(100, data.Length));
return start.Contains("<svg") ||
(start.StartsWith("<?xml") && start.Contains("<svg"));
} }
} }
public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
{ {
/// <summary> /// <summary>
@ -63,7 +231,7 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
/// <summary> /// <summary>
/// Path to places.sqlite /// Path to places.sqlite
/// </summary> /// </summary>
private string PlacesPath private static string PlacesPath
{ {
get get
{ {
@ -77,33 +245,6 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
using var sReader = new StreamReader(profileIni); using var sReader = new StreamReader(profileIni);
var ini = sReader.ReadToEnd(); var ini = sReader.ReadToEnd();
/*
Current profiles.ini structure example as of Firefox version 69.0.1
[Install736426B0AF4A39CB]
Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
Locked=1
[Profile2]
Name=newblahprofile
IsRelative=0
Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
[Profile1]
Name=default
IsRelative=1
Path=Profiles/cydum7q4.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/7789f565.default-release
[General]
StartWithLastProfile=1
Version=2
*/
var lines = ini.Split("\r\n").ToList(); var lines = ini.Split("\r\n").ToList();
var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty; var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>

View file

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.BrowserBookmark.Views; using Flow.Launcher.Plugin.BrowserBookmark.Views;
@ -10,25 +9,34 @@ using System.IO;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.BrowserBookmark; namespace Flow.Launcher.Plugin.BrowserBookmark;
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable
{ {
private static PluginInitContext _context; internal static string _faviconCacheDir;
private static List<Bookmark> _cachedBookmarks = new List<Bookmark>(); internal static PluginInitContext _context;
private static List<Bookmark> _cachedBookmarks = new();
private static Settings _settings; private static Settings _settings;
private static bool _initialized = false; private static bool _initialized = false;
public void Init(PluginInitContext context) public void Init(PluginInitContext context)
{ {
_context = context; _context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>(); _settings = context.API.LoadSettingJsonStorage<Settings>();
_faviconCacheDir = Path.Combine(
context.CurrentPluginMetadata.PluginCacheDirectoryPath,
"FaviconCache");
FilesFolders.ValidateDirectory(_faviconCacheDir);
LoadBookmarksIfEnabled(); LoadBookmarksIfEnabled();
} }
@ -58,7 +66,6 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
// Should top results be returned? (true if no search parameters have been passed) // Should top results be returned? (true if no search parameters have been passed)
var topResults = string.IsNullOrEmpty(param); var topResults = string.IsNullOrEmpty(param);
if (!topResults) if (!topResults)
{ {
// Since we mixed chrome and firefox bookmarks, we should order them again // Since we mixed chrome and firefox bookmarks, we should order them again
@ -68,7 +75,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{ {
Title = c.Name, Title = c.Name,
SubTitle = c.Url, SubTitle = c.Url,
IcoPath = @"Images\bookmark.png", IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath)
? c.FaviconPath
: @"Images\bookmark.png",
Score = BookmarkLoader.MatchProgram(c, param).Score, Score = BookmarkLoader.MatchProgram(c, param).Score,
Action = _ => Action = _ =>
{ {
@ -90,7 +99,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{ {
Title = c.Name, Title = c.Name,
SubTitle = c.Url, SubTitle = c.Url,
IcoPath = @"Images\bookmark.png", IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath)
? c.FaviconPath
: @"Images\bookmark.png",
Score = 5, Score = 5,
Action = _ => Action = _ =>
{ {
@ -104,10 +115,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
} }
} }
private static readonly Channel<byte> _refreshQueue = Channel.CreateBounded<byte>(1);
private static Channel<byte> _refreshQueue = Channel.CreateBounded<byte>(1); private static readonly SemaphoreSlim _fileMonitorSemaphore = new(1, 1);
private static SemaphoreSlim _fileMonitorSemaphore = new(1, 1);
private static async Task MonitorRefreshQueueAsync() private static async Task MonitorRefreshQueueAsync()
{ {
@ -141,12 +151,13 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
return; return;
} }
var watcher = new FileSystemWatcher(directory!); var watcher = new FileSystemWatcher(directory!)
watcher.Filter = Path.GetFileName(path); {
Filter = Path.GetFileName(path),
watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastWrite | NotifyFilters.LastWrite |
NotifyFilters.Size; NotifyFilters.Size
};
watcher.Changed += static (_, _) => watcher.Changed += static (_, _) =>
{ {
@ -195,7 +206,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{ {
return new List<Result>() return new List<Result>()
{ {
new Result new()
{ {
Title = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"), Title = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
SubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"), SubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
@ -210,7 +221,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
catch (Exception e) catch (Exception e)
{ {
var message = "Failed to set url in clipboard"; var message = "Failed to set url in clipboard";
Log.Exception("Main", message, e, "LoadContextMenus"); _context.API.LogException(nameof(Main), message, e);
_context.API.ShowMsg(message); _context.API.ShowMsg(message);

View file

@ -18,4 +18,5 @@ public record Bookmark(string Name, string Url, string Source = "")
} }
public List<CustomBrowser> CustomBrowsers { get; set; } = new(); public List<CustomBrowser> CustomBrowsers { get; set; } = new();
public string FaviconPath { get; set; } = string.Empty;
} }

View file

@ -9,9 +9,14 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Views;
public partial class SettingsControl : INotifyPropertyChanged public partial class SettingsControl : INotifyPropertyChanged
{ {
public Settings Settings { get; } public Settings Settings { get; }
public CustomBrowser SelectedCustomBrowser { get; set; } public CustomBrowser SelectedCustomBrowser { get; set; }
public SettingsControl(Settings settings)
{
Settings = settings;
InitializeComponent();
}
public bool LoadChromeBookmark public bool LoadChromeBookmark
{ {
get => Settings.LoadChromeBookmark; get => Settings.LoadChromeBookmark;
@ -52,12 +57,6 @@ public partial class SettingsControl : INotifyPropertyChanged
} }
} }
public SettingsControl(Settings settings)
{
Settings = settings;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
private void NewCustomBrowser(object sender, RoutedEventArgs e) private void NewCustomBrowser(object sender, RoutedEventArgs e)

View file

@ -2,13 +2,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Linq;
using Flow.Launcher.Plugin.Explorer.Helper; using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.ViewModels;
@ -394,7 +393,7 @@ namespace Flow.Launcher.Plugin.Explorer
{ {
Title = Context.API.GetTranslation("plugin_explorer_excludefromindexsearch"), Title = Context.API.GetTranslation("plugin_explorer_excludefromindexsearch"),
SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath, SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath,
Action = _ => Action = c_ =>
{ {
if (!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => string.Equals(x.Path, record.FullPath, StringComparison.OrdinalIgnoreCase))) if (!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => string.Equals(x.Path, record.FullPath, StringComparison.OrdinalIgnoreCase)))
Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink
@ -402,7 +401,7 @@ namespace Flow.Launcher.Plugin.Explorer
Path = record.FullPath Path = record.FullPath
}); });
Task.Run(() => _ = Task.Run(() =>
{ {
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"), Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
Context.API.GetTranslation("plugin_explorer_path") + Context.API.GetTranslation("plugin_explorer_path") +
@ -470,22 +469,16 @@ namespace Flow.Launcher.Plugin.Explorer
private void LogException(string message, Exception e) private void LogException(string message, Exception e)
{ {
Log.Exception($"|Flow.Launcher.Plugin.Folder.ContextMenu|{message}", e); Context.API.LogException(nameof(ContextMenu), message, e);
} }
private bool CanRunAsDifferentUser(string path) private static bool CanRunAsDifferentUser(string path)
{ {
switch (Path.GetExtension(path)) return Path.GetExtension(path) switch
{ {
case ".exe": ".exe" or ".bat" or ".msi" => true,
case ".bat": _ => false,
case ".msi": };
return true;
default:
return false;
}
} }
} }
} }

View file

@ -1,10 +1,9 @@
using Flow.Launcher.Infrastructure.Logger; using System;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{ {
@ -76,7 +75,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception(nameof(DirectoryInfoSearch), "Error occurred while searching path", e); Main.Context.API.LogException(nameof(DirectoryInfoSearch), "Error occurred while searching path", e);
throw; throw;
} }

View file

@ -1,16 +1,14 @@
using Flow.Launcher.Core.Resource; using System;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using System.Windows.Input;
using Path = System.IO.Path;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Views; using Flow.Launcher.Plugin.Explorer.Views;
using Flow.Launcher.Plugin.SharedCommands;
using Peter; using Peter;
using Path = System.IO.Path;
namespace Flow.Launcher.Plugin.Explorer.Search namespace Flow.Launcher.Plugin.Explorer.Search
{ {
@ -66,7 +64,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
CreateFolderResult(Path.GetFileName(result.FullPath), result.FullPath, result.FullPath, query, result.Score, result.WindowsIndexed), CreateFolderResult(Path.GetFileName(result.FullPath), result.FullPath, result.FullPath, query, result.Score, result.WindowsIndexed),
ResultType.File => ResultType.File =>
CreateFileResult(result.FullPath, query, result.Score, result.WindowsIndexed), CreateFileResult(result.FullPath, query, result.Score, result.WindowsIndexed),
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException(null)
}; };
} }
@ -99,7 +97,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
IcoPath = path, IcoPath = path,
SubTitle = subtitle, SubTitle = subtitle,
AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder), AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, TitleHighlightData = Context.API.FuzzySearch(query.Search, title).MatchData,
CopyText = path, CopyText = path,
Preview = new Result.PreviewInfo Preview = new Result.PreviewInfo
{ {
@ -164,7 +162,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return false; return false;
}, },
Score = score, Score = score,
TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"), TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"),
SubTitleToolTip = path, SubTitleToolTip = path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed } ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }
}; };
@ -286,7 +284,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
FilePath = filePath, FilePath = filePath,
}, },
AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File), AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, TitleHighlightData = Context.API.FuzzySearch(query.Search, title).MatchData,
Score = score, Score = score,
CopyText = filePath, CopyText = filePath,
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, filePath)), PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, filePath)),
@ -319,7 +317,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return true; return true;
}, },
TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"), TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"),
SubTitleToolTip = filePath, SubTitleToolTip = filePath,
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed } ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }
}; };

View file

@ -1,6 +1,4 @@
using Flow.Launcher.Infrastructure.Logger; using System;
using Microsoft.Search.Interop;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.OleDb; using System.Data.OleDb;
using System.Linq; using System.Linq;
@ -9,11 +7,13 @@ using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using Flow.Launcher.Plugin.Explorer.Exceptions; using Flow.Launcher.Plugin.Explorer.Exceptions;
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{ {
internal static class WindowsIndex internal static class WindowsIndex
{ {
private static readonly string ClassName = nameof(WindowsIndex);
// Reserved keywords in oleDB // Reserved keywords in oleDB
private static Regex _reservedPatternMatcher = new(@"^[`\@\\#\\\^,\&\\/\\\$\%_;\[\]]+$", RegexOptions.Compiled); private static Regex _reservedPatternMatcher = new(@"^[`\@\\#\\\^,\&\\/\\\$\%_;\[\]]+$", RegexOptions.Compiled);
@ -33,7 +33,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
} }
catch (OleDbException e) catch (OleDbException e)
{ {
Log.Exception($"|WindowsIndex.ExecuteWindowsIndexSearchAsync|Failed to execute windows index search query: {indexQueryString}", e); Main.Context.API.LogException(ClassName, $"Failed to execute windows index search query: {indexQueryString}", e);
yield break; yield break;
} }
await using var dataReader = dataReaderAttempt; await using var dataReader = dataReaderAttempt;

View file

@ -41,8 +41,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup> </ItemGroup>

View file

@ -1,20 +1,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Flow.Launcher.Core.Plugin;
namespace Flow.Launcher.Plugin.PluginIndicator namespace Flow.Launcher.Plugin.PluginIndicator
{ {
public class Main : IPlugin, IPluginI18n public class Main : IPlugin, IPluginI18n
{ {
private PluginInitContext context; internal PluginInitContext Context { get; private set; }
public List<Result> Query(Query query) public List<Result> Query(Query query)
{ {
var nonGlobalPlugins = GetNonGlobalPlugins();
var results = var results =
from keyword in PluginManager.NonGlobalPlugins.Keys from keyword in nonGlobalPlugins.Keys
let plugin = PluginManager.NonGlobalPlugins[keyword].Metadata let plugin = nonGlobalPlugins[keyword].Metadata
let keywordSearchResult = context.API.FuzzySearch(query.Search, keyword) let keywordSearchResult = Context.API.FuzzySearch(query.Search, keyword)
let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : context.API.FuzzySearch(query.Search, plugin.Name) let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(query.Search, plugin.Name)
let score = searchResult.Score let score = searchResult.Score
where (searchResult.IsSearchPrecisionScoreMet() where (searchResult.IsSearchPrecisionScoreMet()
|| string.IsNullOrEmpty(query.Search)) // To list all available action keywords || string.IsNullOrEmpty(query.Search)) // To list all available action keywords
@ -22,32 +22,51 @@ namespace Flow.Launcher.Plugin.PluginIndicator
select new Result select new Result
{ {
Title = keyword, Title = keyword,
SubTitle = string.Format(context.API.GetTranslation("flowlauncher_plugin_pluginindicator_result_subtitle"), plugin.Name), SubTitle = string.Format(Context.API.GetTranslation("flowlauncher_plugin_pluginindicator_result_subtitle"), plugin.Name),
Score = score, Score = score,
IcoPath = plugin.IcoPath, IcoPath = plugin.IcoPath,
AutoCompleteText = $"{keyword}{Plugin.Query.TermSeparator}", AutoCompleteText = $"{keyword}{Plugin.Query.TermSeparator}",
Action = c => Action = c =>
{ {
context.API.ChangeQuery($"{keyword}{Plugin.Query.TermSeparator}"); Context.API.ChangeQuery($"{keyword}{Plugin.Query.TermSeparator}");
return false; return false;
} }
}; };
return results.ToList(); return results.ToList();
} }
private Dictionary<string, PluginPair> GetNonGlobalPlugins()
{
var nonGlobalPlugins = new Dictionary<string, PluginPair>();
foreach (var plugin in Context.API.GetAllPlugins())
{
foreach (var actionKeyword in plugin.Metadata.ActionKeywords)
{
// Skip global keywords
if (actionKeyword == Plugin.Query.GlobalPluginWildcardSign) continue;
// Skip dulpicated keywords
if (nonGlobalPlugins.ContainsKey(actionKeyword)) continue;
nonGlobalPlugins.Add(actionKeyword, plugin);
}
}
return nonGlobalPlugins;
}
public void Init(PluginInitContext context) public void Init(PluginInitContext context)
{ {
this.context = context; Context = context;
} }
public string GetTranslatedPluginTitle() public string GetTranslatedPluginTitle()
{ {
return context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_name"); return Context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_name");
} }
public string GetTranslatedPluginDescription() public string GetTranslatedPluginDescription()
{ {
return context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_description"); return Context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_description");
} }
} }
} }

View file

@ -18,6 +18,8 @@ namespace Flow.Launcher.Plugin.Program
{ {
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable
{ {
private static readonly string ClassName = nameof(Main);
private const string Win32CacheName = "Win32"; private const string Win32CacheName = "Win32";
private const string UwpCacheName = "UWP"; private const string UwpCacheName = "UWP";
@ -30,8 +32,6 @@ namespace Flow.Launcher.Plugin.Program
internal static PluginInitContext Context { get; private set; } internal static PluginInitContext Context { get; private set; }
private static readonly string ClassName = nameof(Main);
private static readonly List<Result> emptyResults = new(); private static readonly List<Result> emptyResults = new();
private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 }; private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
@ -274,7 +274,7 @@ namespace Flow.Launcher.Plugin.Program
static void WatchProgramUpdate() static void WatchProgramUpdate()
{ {
Win32.WatchProgramUpdate(_settings); Win32.WatchProgramUpdate(_settings);
_ = UWPPackage.WatchPackageChange(); _ = UWPPackage.WatchPackageChangeAsync();
} }
} }

View file

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using Windows.Management.Deployment; using Windows.Management.Deployment;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using System.Threading.Channels; using System.Threading.Channels;
@ -290,9 +289,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
private static Channel<byte> PackageChangeChannel = Channel.CreateBounded<byte>(1); private static readonly Channel<byte> PackageChangeChannel = Channel.CreateBounded<byte>(1);
public static async Task WatchPackageChange() public static async Task WatchPackageChangeAsync()
{ {
if (Environment.OSVersion.Version.Major >= 10) if (Environment.OSVersion.Version.Major >= 10)
{ {
@ -403,13 +402,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description)) if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description))
{ {
title = Name; title = Name;
matchResult = StringMatcher.FuzzySearch(query, Name); matchResult = Main.Context.API.FuzzySearch(query, Name);
} }
else else
{ {
title = $"{Name}: {Description}"; title = $"{Name}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, Name); var nameMatch = Main.Context.API.FuzzySearch(query, Name);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description); var descriptionMatch = Main.Context.API.FuzzySearch(query, Description);
if (descriptionMatch.Score > nameMatch.Score) if (descriptionMatch.Score > nameMatch.Score)
{ {
for (int i = 0; i < descriptionMatch.MatchData.Count; i++) for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
@ -477,7 +476,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
var contextMenus = new List<Result> var contextMenus = new List<Result>
{ {
new Result new()
{ {
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Action = _ => Action = _ =>
@ -496,9 +495,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
contextMenus.Add(new Result contextMenus.Add(new Result
{ {
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
Action = _ => Action = c =>
{ {
Task.Run(() => Launch(true)).ConfigureAwait(false); _ = Task.Run(() => Launch(true)).ConfigureAwait(false);
return true; return true;
}, },
IcoPath = "Images/cmd.png", IcoPath = "Images/cmd.png",
@ -539,7 +538,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|{UserModelId} 's logo uri is null or empty: {Location}", $"|{UserModelId} 's logo uri is null or empty: {Location}",
new ArgumentException("uri")); new ArgumentException(null, nameof(uri)));
return string.Empty; return string.Empty;
} }

View file

@ -6,7 +6,6 @@ using System.Security;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Win32; using Microsoft.Win32;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
@ -73,7 +72,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
private const string ExeExtension = "exe"; private const string ExeExtension = "exe";
private string _uid = string.Empty; private string _uid = string.Empty;
private static readonly Win32 Default = new Win32() private static readonly Win32 Default = new()
{ {
Name = string.Empty, Name = string.Empty,
Description = string.Empty, Description = string.Empty,
@ -92,7 +91,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (candidates.Count == 0) if (candidates.Count == 0)
return null; return null;
var match = candidates.Select(candidate => StringMatcher.FuzzySearch(query, candidate)) var match = candidates.Select(candidate => Main.Context.API.FuzzySearch(query, candidate))
.MaxBy(match => match.Score); .MaxBy(match => match.Score);
return match?.IsSearchPrecisionScoreMet() ?? false ? match : null; return match?.IsSearchPrecisionScoreMet() ?? false ? match : null;
@ -112,14 +111,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
resultName.Equals(Description)) resultName.Equals(Description))
{ {
title = resultName; title = resultName;
matchResult = StringMatcher.FuzzySearch(query, resultName); matchResult = Main.Context.API.FuzzySearch(query, resultName);
} }
else else
{ {
// Search in both // Search in both
title = $"{resultName}: {Description}"; title = $"{resultName}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, resultName); var nameMatch = Main.Context.API.FuzzySearch(query, resultName);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description); var descriptionMatch = Main.Context.API.FuzzySearch(query, Description);
if (descriptionMatch.Score > nameMatch.Score) if (descriptionMatch.Score > nameMatch.Score)
{ {
for (int i = 0; i < descriptionMatch.MatchData.Count; i++) for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
@ -219,27 +218,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
var contextMenus = new List<Result> var contextMenus = new List<Result>
{ {
new Result new()
{ {
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_different_user"), Title = api.GetTranslation("flowlauncher_plugin_program_run_as_different_user"),
Action = _ => Action = c =>
{ {
var info = new ProcessStartInfo var info = new ProcessStartInfo
{ {
FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true
}; };
Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); _ = Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info));
return true; return true;
}, },
IcoPath = "Images/user.png", IcoPath = "Images/user.png",
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ee"), Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ee"),
}, },
new Result new()
{ {
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
Action = _ => Action = c =>
{ {
var info = new ProcessStartInfo var info = new ProcessStartInfo
{ {
@ -249,14 +248,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
UseShellExecute = true UseShellExecute = true
}; };
Task.Run(() => Main.StartProcess(Process.Start, info)); _ = Task.Run(() => Main.StartProcess(Process.Start, info));
return true; return true;
}, },
IcoPath = "Images/cmd.png", IcoPath = "Images/cmd.png",
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef"), Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef"),
}, },
new Result new()
{ {
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Action = _ => Action = _ =>
@ -296,7 +295,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Name; return Name;
} }
private static List<FileSystemWatcher> Watchers = new List<FileSystemWatcher>(); private static readonly List<FileSystemWatcher> Watchers = new();
private static Win32 Win32Program(string path) private static Win32 Win32Program(string path)
{ {
@ -402,7 +401,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var data = parser.ReadFile(path); var data = parser.ReadFile(path);
var urlSection = data["InternetShortcut"]; var urlSection = data["InternetShortcut"];
var url = urlSection?["URL"]; var url = urlSection?["URL"];
if (String.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
{ {
return program; return program;
} }
@ -418,12 +417,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
var iconPath = urlSection?["IconFile"]; var iconPath = urlSection?["IconFile"];
if (!String.IsNullOrEmpty(iconPath)) if (!string.IsNullOrEmpty(iconPath))
{ {
program.IcoPath = iconPath; program.IcoPath = iconPath;
} }
} }
catch (Exception e) catch (Exception)
{ {
// Many files do not have the required fields, so no logging is done. // Many files do not have the required fields, so no logging is done.
} }
@ -474,7 +473,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var extension = Path.GetExtension(path)?.ToLowerInvariant(); var extension = Path.GetExtension(path)?.ToLowerInvariant();
if (!string.IsNullOrEmpty(extension)) if (!string.IsNullOrEmpty(extension))
{ {
return extension.Substring(1); // remove dot return extension[1..]; // remove dot
} }
else else
{ {
@ -785,7 +784,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
_ = Task.Run(MonitorDirectoryChangeAsync); _ = Task.Run(MonitorDirectoryChangeAsync);
} }
private static Channel<byte> indexQueue = Channel.CreateBounded<byte>(1); private static readonly Channel<byte> indexQueue = Channel.CreateBounded<byte>(1);
public static async Task MonitorDirectoryChangeAsync() public static async Task MonitorDirectoryChangeAsync()
{ {

View file

@ -37,7 +37,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup> </ItemGroup>

View file

@ -7,20 +7,21 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using WindowsInput; using WindowsInput;
using WindowsInput.Native; using WindowsInput.Native;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Control = System.Windows.Controls.Control; using Control = System.Windows.Controls.Control;
using Keys = System.Windows.Forms.Keys; using Keys = System.Windows.Forms.Keys;
namespace Flow.Launcher.Plugin.Shell namespace Flow.Launcher.Plugin.Shell
{ {
public class Main : IPlugin, ISettingProvider, IPluginI18n, IContextMenu public class Main : IPlugin, ISettingProvider, IPluginI18n, IContextMenu, IDisposable
{ {
private static readonly string ClassName = nameof(Main);
internal PluginInitContext Context { get; private set; }
private const string Image = "Images/shell.png"; private const string Image = "Images/shell.png";
private PluginInitContext context;
private bool _winRStroked; private bool _winRStroked;
private readonly KeyboardSimulator _keyboardSimulator = new KeyboardSimulator(new InputSimulator()); private readonly KeyboardSimulator _keyboardSimulator = new(new InputSimulator());
private Settings _settings; private Settings _settings;
@ -53,7 +54,7 @@ namespace Flow.Launcher.Plugin.Shell
{ {
basedir = Path.GetDirectoryName(excmd); basedir = Path.GetDirectoryName(excmd);
var dirName = Path.GetDirectoryName(cmd); var dirName = Path.GetDirectoryName(cmd);
dir = (dirName.EndsWith("/") || dirName.EndsWith(@"\")) ? dirName : cmd.Substring(0, dirName.Length + 1); dir = (dirName.EndsWith("/") || dirName.EndsWith(@"\")) ? dirName : cmd[..(dirName.Length + 1)];
} }
if (basedir != null) if (basedir != null)
@ -88,7 +89,7 @@ namespace Flow.Launcher.Plugin.Shell
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|Flow.Launcher.Plugin.Shell.Main.Query|Exception when query for <{query}>", e); Context.API.LogException(ClassName, $"Exception when query for <{query}>", e);
} }
return results; return results;
} }
@ -102,14 +103,14 @@ namespace Flow.Launcher.Plugin.Shell
{ {
if (m.Key == cmd) if (m.Key == cmd)
{ {
result.SubTitle = string.Format(context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value); result.SubTitle = string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value);
return null; return null;
} }
var ret = new Result var ret = new Result
{ {
Title = m.Key, Title = m.Key,
SubTitle = string.Format(context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value), SubTitle = string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value),
IcoPath = Image, IcoPath = Image,
Action = c => Action = c =>
{ {
@ -139,7 +140,7 @@ namespace Flow.Launcher.Plugin.Shell
{ {
Title = cmd, Title = cmd,
Score = 5000, Score = 5000,
SubTitle = context.API.GetTranslation("flowlauncher_plugin_cmd_execute_through_shell"), SubTitle = Context.API.GetTranslation("flowlauncher_plugin_cmd_execute_through_shell"),
IcoPath = Image, IcoPath = Image,
Action = c => Action = c =>
{ {
@ -164,7 +165,7 @@ namespace Flow.Launcher.Plugin.Shell
.Select(m => new Result .Select(m => new Result
{ {
Title = m.Key, Title = m.Key,
SubTitle = string.Format(context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value), SubTitle = string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_cmd_has_been_executed_times"), m.Value),
IcoPath = Image, IcoPath = Image,
Action = c => Action = c =>
{ {
@ -211,7 +212,7 @@ namespace Flow.Launcher.Plugin.Shell
info.FileName = "cmd.exe"; info.FileName = "cmd.exe";
} }
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}"); info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break; break;
} }
@ -234,7 +235,7 @@ namespace Flow.Launcher.Plugin.Shell
else else
{ {
info.ArgumentList.Add("-Command"); info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}"); info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
} }
break; break;
} }
@ -255,7 +256,7 @@ namespace Flow.Launcher.Plugin.Shell
info.ArgumentList.Add("-NoExit"); info.ArgumentList.Add("-NoExit");
} }
info.ArgumentList.Add("-Command"); info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}"); info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
break; break;
} }
@ -309,17 +310,17 @@ namespace Flow.Launcher.Plugin.Shell
{ {
var name = "Plugin: Shell"; var name = "Plugin: Shell";
var message = $"Command not found: {e.Message}"; var message = $"Command not found: {e.Message}";
context.API.ShowMsg(name, message); Context.API.ShowMsg(name, message);
} }
catch (Win32Exception e) catch (Win32Exception e)
{ {
var name = "Plugin: Shell"; var name = "Plugin: Shell";
var message = $"Error running the command: {e.Message}"; var message = $"Error running the command: {e.Message}";
context.API.ShowMsg(name, message); Context.API.ShowMsg(name, message);
} }
} }
private bool ExistInPath(string filename) private static bool ExistInPath(string filename)
{ {
if (File.Exists(filename)) if (File.Exists(filename))
{ {
@ -350,14 +351,14 @@ namespace Flow.Launcher.Plugin.Shell
public void Init(PluginInitContext context) public void Init(PluginInitContext context)
{ {
this.context = context; Context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>(); _settings = context.API.LoadSettingJsonStorage<Settings>();
context.API.RegisterGlobalKeyboardCallback(API_GlobalKeyboardEvent); context.API.RegisterGlobalKeyboardCallback(API_GlobalKeyboardEvent);
} }
bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state) bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)
{ {
if (!context.CurrentPluginMetadata.Disabled && _settings.ReplaceWinR) if (!Context.CurrentPluginMetadata.Disabled && _settings.ReplaceWinR)
{ {
if (keyevent == (int)KeyEvent.WM_KEYDOWN && vkcode == (int)Keys.R && state.WinPressed) if (keyevent == (int)KeyEvent.WM_KEYDOWN && vkcode == (int)Keys.R && state.WinPressed)
{ {
@ -380,10 +381,9 @@ namespace Flow.Launcher.Plugin.Shell
// show the main window and set focus to the query box // show the main window and set focus to the query box
_ = Task.Run(() => _ = Task.Run(() =>
{ {
context.API.ShowMainWindow(); Context.API.ShowMainWindow();
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeparator}"); Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeparator}");
}); });
} }
public Control CreateSettingPanel() public Control CreateSettingPanel()
@ -393,12 +393,12 @@ namespace Flow.Launcher.Plugin.Shell
public string GetTranslatedPluginTitle() public string GetTranslatedPluginTitle()
{ {
return context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_name"); return Context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_name");
} }
public string GetTranslatedPluginDescription() public string GetTranslatedPluginDescription()
{ {
return context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_description"); return Context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_description");
} }
public List<Result> LoadContextMenus(Result selectedResult) public List<Result> LoadContextMenus(Result selectedResult)
@ -407,8 +407,8 @@ namespace Flow.Launcher.Plugin.Shell
{ {
new() new()
{ {
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_different_user"), Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_different_user"),
AsyncAction = async c => Action = c =>
{ {
Execute(ShellCommand.RunAsDifferentUser, PrepareProcessStartInfo(selectedResult.Title)); Execute(ShellCommand.RunAsDifferentUser, PrepareProcessStartInfo(selectedResult.Title));
return true; return true;
@ -418,7 +418,7 @@ namespace Flow.Launcher.Plugin.Shell
}, },
new() new()
{ {
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_administrator"), Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_administrator"),
Action = c => Action = c =>
{ {
Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, true)); Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, true));
@ -429,10 +429,10 @@ namespace Flow.Launcher.Plugin.Shell
}, },
new() new()
{ {
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_copy"), Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_copy"),
Action = c => Action = c =>
{ {
context.API.CopyToClipboard(selectedResult.Title); Context.API.CopyToClipboard(selectedResult.Title);
return true; return true;
}, },
IcoPath = "Images/copy.png", IcoPath = "Images/copy.png",
@ -442,5 +442,10 @@ namespace Flow.Launcher.Plugin.Shell
return results; return results;
} }
public void Dispose()
{
Context.API.RemoveGlobalKeyboardCallback(API_GlobalKeyboardEvent);
}
} }
} }

View file

@ -39,7 +39,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
@ -19,6 +18,8 @@ namespace Flow.Launcher.Plugin.Sys
{ {
public class Main : IPlugin, ISettingProvider, IPluginI18n public class Main : IPlugin, ISettingProvider, IPluginI18n
{ {
private static readonly string ClassName = nameof(Main);
private readonly Dictionary<string, string> KeywordTitleMappings = new() private readonly Dictionary<string, string> KeywordTitleMappings = new()
{ {
{"Shutdown", "flowlauncher_plugin_sys_shutdown_computer_cmd"}, {"Shutdown", "flowlauncher_plugin_sys_shutdown_computer_cmd"},
@ -106,7 +107,7 @@ namespace Flow.Launcher.Plugin.Sys
{ {
if (!KeywordTitleMappings.TryGetValue(key, out var translationKey)) if (!KeywordTitleMappings.TryGetValue(key, out var translationKey))
{ {
Log.Error("Flow.Launcher.Plugin.Sys.Main", $"Title not found for: {key}"); _context.API.LogError(ClassName, $"Title not found for: {key}");
return "Title Not Found"; return "Title Not Found";
} }
@ -117,7 +118,7 @@ namespace Flow.Launcher.Plugin.Sys
{ {
if (!KeywordDescriptionMappings.TryGetValue(key, out var translationKey)) if (!KeywordDescriptionMappings.TryGetValue(key, out var translationKey))
{ {
Log.Error("Flow.Launcher.Plugin.Sys.Main", $"Description not found for: {key}"); _context.API.LogError(ClassName, $"Description not found for: {key}");
return "Description Not Found"; return "Description Not Found";
} }

View file

@ -51,7 +51,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup> </ItemGroup>