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
{
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
public class LocalizationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

View file

@ -1,15 +1,19 @@
using System.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource
{
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;
public LocalizedDescriptionAttribute(string resourceKey)
{
_translator = InternationalizationManager.Instance;
_resourceKey = resourceKey;
}
@ -17,7 +21,7 @@ namespace Flow.Launcher.Core.Resource
{
get
{
string description = _translator.GetTranslation(_resourceKey);
string description = API.GetTranslation(_resourceKey);
return string.IsNullOrWhiteSpace(description) ?
string.Format("[[{0}]]", _resourceKey) : description;
}

View file

@ -1,19 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource
{
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)
{
var key = value.ToString();
if (String.IsNullOrEmpty(key))
return key;
return InternationalizationManager.Instance.GetTranslation(key);
if (string.IsNullOrEmpty(key)) return key;
return API.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 Include="NLog" Version="4.7.10" />
<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" />
<!--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-->

View file

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

View file

@ -10,6 +10,8 @@ using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;
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 const int SmallIconSize = 64;
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 SvgExtension = ".svg";
public static async Task InitializeAsync()
{
@ -235,10 +239,11 @@ namespace Flow.Launcher.Infrastructure.Image
image = LoadFullImage(path);
type = ImageType.FullImageFile;
}
catch (NotSupportedException)
catch (NotSupportedException ex)
{
image = Image;
type = ImageType.Error;
API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
}
}
else
@ -251,6 +256,20 @@ namespace Flow.Launcher.Infrastructure.Image
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
{
type = ImageType.File;
@ -324,7 +343,7 @@ namespace Flow.Launcher.Infrastructure.Image
return img;
}
private static BitmapImage LoadFullImage(string path)
private static ImageSource LoadFullImage(string path)
{
BitmapImage image = new BitmapImage();
image.BeginInit();
@ -333,24 +352,24 @@ namespace Flow.Launcher.Infrastructure.Image
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit();
if (image.PixelWidth > 320)
if (image.PixelWidth > FullImageSize)
{
BitmapImage resizedWidth = new BitmapImage();
resizedWidth.BeginInit();
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
resizedWidth.UriSource = new Uri(path);
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedWidth.DecodePixelWidth = 320;
resizedWidth.DecodePixelWidth = FullImageSize;
resizedWidth.EndInit();
if (resizedWidth.PixelHeight > 320)
if (resizedWidth.PixelHeight > FullImageSize)
{
BitmapImage resizedHeight = new BitmapImage();
resizedHeight.BeginInit();
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
resizedHeight.UriSource = new Uri(path);
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedHeight.DecodePixelHeight = 320;
resizedHeight.DecodePixelHeight = FullImageSize;
resizedHeight.EndInit();
return resizedHeight;
}
@ -360,5 +379,50 @@ namespace Flow.Launcher.Infrastructure.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
VIRTUAL_KEY
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
EnumWindows
DwmSetWindowAttribute

View file

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

View file

@ -142,15 +142,47 @@ namespace Flow.Launcher.Plugin
List<PluginPair> GetAllPlugins();
/// <summary>
/// Register a callback for Global Keyboard Event
/// Registers a callback function for global keyboard events.
/// </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);
/// <summary>
/// Remove a callback for Global Keyboard Event
/// </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);
/// <summary>
@ -372,6 +404,7 @@ namespace Flow.Launcher.Plugin
/// </returns>
public bool SetCurrentTheme(ThemeData theme);
/// <summary>
/// Save all Flow's plugins caches
/// </summary>
void SavePluginCaches();
@ -404,7 +437,10 @@ namespace Flow.Launcher.Plugin
/// </remarks>
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.
/// </summary>
/// <param name="path">The path of the image.</param>
@ -418,6 +454,7 @@ namespace Flow.Launcher.Plugin
/// <returns></returns>
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
/// <summary>
/// Update the plugin manifest
/// </summary>
/// <param name="usePrimaryUrlOnly">

View file

@ -1,7 +1,12 @@
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
{
/// <summary>

View file

@ -1,3 +1,8 @@
EnumThreadWindows
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)
{
return string.IsNullOrEmpty(FilterText) ||
StringMatcher.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
StringMatcher.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
}
}

View file

@ -106,8 +106,8 @@ public partial class SettingsPanePluginsViewModel : BaseModel
public List<PluginViewModel> FilteredPluginViewModels => PluginViewModels
.Where(v =>
string.IsNullOrEmpty(FilterText) ||
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
)
.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.Text.Json;
using Flow.Launcher.Infrastructure.Logger;
using System;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
namespace Flow.Launcher.Plugin.BrowserBookmark;
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();
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
@ -22,16 +32,36 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
if (!File.Exists(bookmarkPath))
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)})");
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;
}
protected List<Bookmark> LoadBookmarksFromFile(string path, string source)
protected static List<Bookmark> LoadBookmarksFromFile(string path, string source)
{
var bookmarks = new List<Bookmark>();
@ -45,15 +75,14 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
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())
{
if (folder.Value.ValueKind != JsonValueKind.Object)
continue;
// Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details.
// If various exceptions start to build up here consider splitting this Loader into multiple separate ones.
// Fix for Opera. It stores bookmarks slightly different than chrome.
if (folder.Name == "custom_root")
EnumerateRoot(folder.Value, bookmarks, source);
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)
{
if (!folderElement.TryGetProperty("children", out var childrenElement))
@ -86,9 +115,107 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
}
else
{
Log.Error(
$"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}");
Main._context.API.LogError(ClassName, $"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.Linq;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.SharedModels;
@ -10,11 +9,11 @@ internal static class BookmarkLoader
{
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())
return match;
return StringMatcher.FuzzySearch(queryString, bookmark.Url);
return Main._context.API.FuzzySearch(queryString, bookmark.Url);
}
internal static List<Bookmark> LoadAllBookmarks(Settings setting)

View file

@ -1,16 +1,26 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
namespace Flow.Launcher.Plugin.BrowserBookmark;
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();
// Updated query - removed favicon_id column
private const string QueryAllBookmarks = """
SELECT moz_places.url, moz_bookmarks.title
FROM moz_places
@ -20,36 +30,194 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
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))
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
string dbPath = string.Format(DbPathFormat, placesPath);
using var dbConnection = new SqliteConnection(dbPath);
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
try
{
// Try to register file monitoring
try
{
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
return reader
.Select(
x => new Bookmark(
x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString()
// Use a copy to avoid lock issues with the original file
File.Copy(placesPath, tempDbPath, true);
// Connect to database and execute query
string dbPath = string.Format(DbPathFormat, tempDbPath);
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
{
/// <summary>
@ -63,7 +231,7 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
/// <summary>
/// Path to places.sqlite
/// </summary>
private string PlacesPath
private static string PlacesPath
{
get
{
@ -77,33 +245,6 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
using var sReader = new StreamReader(profileIni);
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 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>
<OutputType>Library</OutputType>

View file

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.BrowserBookmark.Views;
@ -10,25 +9,34 @@ using System.IO;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.BrowserBookmark;
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 bool _initialized = false;
public void Init(PluginInitContext context)
{
_context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>();
_faviconCacheDir = Path.Combine(
context.CurrentPluginMetadata.PluginCacheDirectoryPath,
"FaviconCache");
FilesFolders.ValidateDirectory(_faviconCacheDir);
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)
var topResults = string.IsNullOrEmpty(param);
if (!topResults)
{
// 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,
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,
Action = _ =>
{
@ -90,7 +99,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
IcoPath = !string.IsNullOrEmpty(c.FaviconPath) && File.Exists(c.FaviconPath)
? c.FaviconPath
: @"Images\bookmark.png",
Score = 5,
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 SemaphoreSlim _fileMonitorSemaphore = new(1, 1);
private static readonly SemaphoreSlim _fileMonitorSemaphore = new(1, 1);
private static async Task MonitorRefreshQueueAsync()
{
@ -141,12 +151,13 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
return;
}
var watcher = new FileSystemWatcher(directory!);
watcher.Filter = Path.GetFileName(path);
watcher.NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size;
var watcher = new FileSystemWatcher(directory!)
{
Filter = Path.GetFileName(path),
NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size
};
watcher.Changed += static (_, _) =>
{
@ -195,7 +206,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
{
return new List<Result>()
{
new Result
new()
{
Title = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
SubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
@ -210,7 +221,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
catch (Exception e)
{
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);

View file

@ -18,4 +18,5 @@ public record Bookmark(string Name, string Url, string Source = "")
}
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 Settings Settings { get; }
public CustomBrowser SelectedCustomBrowser { get; set; }
public SettingsControl(Settings settings)
{
Settings = settings;
InitializeComponent();
}
public bool LoadChromeBookmark
{
get => Settings.LoadChromeBookmark;
@ -52,12 +57,6 @@ public partial class SettingsControl : INotifyPropertyChanged
}
}
public SettingsControl(Settings settings)
{
Settings = settings;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NewCustomBrowser(object sender, RoutedEventArgs e)

View file

@ -2,13 +2,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Linq;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.ViewModels;
@ -394,7 +393,7 @@ namespace Flow.Launcher.Plugin.Explorer
{
Title = Context.API.GetTranslation("plugin_explorer_excludefromindexsearch"),
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)))
Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink
@ -402,7 +401,7 @@ namespace Flow.Launcher.Plugin.Explorer
Path = record.FullPath
});
Task.Run(() =>
_ = Task.Run(() =>
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
Context.API.GetTranslation("plugin_explorer_path") +
@ -470,22 +469,16 @@ namespace Flow.Launcher.Plugin.Explorer
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":
case ".bat":
case ".msi":
return true;
default:
return false;
}
".exe" or ".bat" or ".msi" => true,
_ => false,
};
}
}
}

View file

@ -1,10 +1,9 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
@ -76,7 +75,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
}
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;
}

View file

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

View file

@ -1,6 +1,4 @@
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Search.Interop;
using System;
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
@ -9,11 +7,13 @@ using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
internal static class WindowsIndex
{
private static readonly string ClassName = nameof(WindowsIndex);
// Reserved keywords in oleDB
private static Regex _reservedPatternMatcher = new(@"^[`\@\\#\\\^,\&\\/\\\$\%_;\[\]]+$", RegexOptions.Compiled);
@ -33,7 +33,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
}
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;
}
await using var dataReader = dataReaderAttempt;

View file

@ -41,8 +41,6 @@
</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" />
</ItemGroup>

View file

@ -1,20 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using Flow.Launcher.Core.Plugin;
namespace Flow.Launcher.Plugin.PluginIndicator
{
public class Main : IPlugin, IPluginI18n
{
private PluginInitContext context;
internal PluginInitContext Context { get; private set; }
public List<Result> Query(Query query)
{
var nonGlobalPlugins = GetNonGlobalPlugins();
var results =
from keyword in PluginManager.NonGlobalPlugins.Keys
let plugin = PluginManager.NonGlobalPlugins[keyword].Metadata
let keywordSearchResult = context.API.FuzzySearch(query.Search, keyword)
let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : context.API.FuzzySearch(query.Search, plugin.Name)
from keyword in nonGlobalPlugins.Keys
let plugin = nonGlobalPlugins[keyword].Metadata
let keywordSearchResult = Context.API.FuzzySearch(query.Search, keyword)
let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(query.Search, plugin.Name)
let score = searchResult.Score
where (searchResult.IsSearchPrecisionScoreMet()
|| string.IsNullOrEmpty(query.Search)) // To list all available action keywords
@ -22,32 +22,51 @@ namespace Flow.Launcher.Plugin.PluginIndicator
select new Result
{
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,
IcoPath = plugin.IcoPath,
AutoCompleteText = $"{keyword}{Plugin.Query.TermSeparator}",
Action = c =>
{
context.API.ChangeQuery($"{keyword}{Plugin.Query.TermSeparator}");
Context.API.ChangeQuery($"{keyword}{Plugin.Query.TermSeparator}");
return false;
}
};
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)
{
this.context = context;
Context = context;
}
public string GetTranslatedPluginTitle()
{
return context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_name");
return Context.API.GetTranslation("flowlauncher_plugin_pluginindicator_plugin_name");
}
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
{
private static readonly string ClassName = nameof(Main);
private const string Win32CacheName = "Win32";
private const string UwpCacheName = "UWP";
@ -30,8 +32,6 @@ namespace Flow.Launcher.Plugin.Program
internal static PluginInitContext Context { get; private set; }
private static readonly string ClassName = nameof(Main);
private static readonly List<Result> emptyResults = new();
private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
@ -274,7 +274,7 @@ namespace Flow.Launcher.Plugin.Program
static void WatchProgramUpdate()
{
Win32.WatchProgramUpdate(_settings);
_ = UWPPackage.WatchPackageChange();
_ = UWPPackage.WatchPackageChangeAsync();
}
}

View file

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using Windows.ApplicationModel;
using Windows.Management.Deployment;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedModels;
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)
{
@ -403,13 +402,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description))
{
title = Name;
matchResult = StringMatcher.FuzzySearch(query, Name);
matchResult = Main.Context.API.FuzzySearch(query, Name);
}
else
{
title = $"{Name}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, Name);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
var nameMatch = Main.Context.API.FuzzySearch(query, Name);
var descriptionMatch = Main.Context.API.FuzzySearch(query, Description);
if (descriptionMatch.Score > nameMatch.Score)
{
for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
@ -477,7 +476,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var contextMenus = new List<Result>
{
new Result
new()
{
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Action = _ =>
@ -496,9 +495,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
contextMenus.Add(new Result
{
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;
},
IcoPath = "Images/cmd.png",
@ -539,7 +538,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|{UserModelId} 's logo uri is null or empty: {Location}",
new ArgumentException("uri"));
new ArgumentException(null, nameof(uri)));
return string.Empty;
}

View file

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

View file

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

View file

@ -7,20 +7,21 @@ using System.Linq;
using System.Threading.Tasks;
using WindowsInput;
using WindowsInput.Native;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Control = System.Windows.Controls.Control;
using Keys = System.Windows.Forms.Keys;
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 PluginInitContext context;
private bool _winRStroked;
private readonly KeyboardSimulator _keyboardSimulator = new KeyboardSimulator(new InputSimulator());
private readonly KeyboardSimulator _keyboardSimulator = new(new InputSimulator());
private Settings _settings;
@ -53,7 +54,7 @@ namespace Flow.Launcher.Plugin.Shell
{
basedir = Path.GetDirectoryName(excmd);
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)
@ -88,7 +89,7 @@ namespace Flow.Launcher.Plugin.Shell
}
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;
}
@ -102,14 +103,14 @@ namespace Flow.Launcher.Plugin.Shell
{
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;
}
var ret = new Result
{
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,
Action = c =>
{
@ -139,7 +140,7 @@ namespace Flow.Launcher.Plugin.Shell
{
Title = cmd,
Score = 5000,
SubTitle = context.API.GetTranslation("flowlauncher_plugin_cmd_execute_through_shell"),
SubTitle = Context.API.GetTranslation("flowlauncher_plugin_cmd_execute_through_shell"),
IcoPath = Image,
Action = c =>
{
@ -164,7 +165,7 @@ namespace Flow.Launcher.Plugin.Shell
.Select(m => new Result
{
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,
Action = c =>
{
@ -211,7 +212,7 @@ namespace Flow.Launcher.Plugin.Shell
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;
}
@ -234,7 +235,7 @@ namespace Flow.Launcher.Plugin.Shell
else
{
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;
}
@ -255,7 +256,7 @@ namespace Flow.Launcher.Plugin.Shell
info.ArgumentList.Add("-NoExit");
}
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;
}
@ -309,17 +310,17 @@ namespace Flow.Launcher.Plugin.Shell
{
var name = "Plugin: Shell";
var message = $"Command not found: {e.Message}";
context.API.ShowMsg(name, message);
Context.API.ShowMsg(name, message);
}
catch (Win32Exception e)
{
var name = "Plugin: Shell";
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))
{
@ -350,14 +351,14 @@ namespace Flow.Launcher.Plugin.Shell
public void Init(PluginInitContext context)
{
this.context = context;
Context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>();
context.API.RegisterGlobalKeyboardCallback(API_GlobalKeyboardEvent);
}
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)
{
@ -380,10 +381,9 @@ namespace Flow.Launcher.Plugin.Shell
// show the main window and set focus to the query box
_ = Task.Run(() =>
{
context.API.ShowMainWindow();
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeparator}");
Context.API.ShowMainWindow();
Context.API.ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeparator}");
});
}
public Control CreateSettingPanel()
@ -393,12 +393,12 @@ namespace Flow.Launcher.Plugin.Shell
public string GetTranslatedPluginTitle()
{
return context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_name");
return Context.API.GetTranslation("flowlauncher_plugin_cmd_plugin_name");
}
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)
@ -407,8 +407,8 @@ namespace Flow.Launcher.Plugin.Shell
{
new()
{
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_different_user"),
AsyncAction = async c =>
Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_different_user"),
Action = c =>
{
Execute(ShellCommand.RunAsDifferentUser, PrepareProcessStartInfo(selectedResult.Title));
return true;
@ -418,7 +418,7 @@ namespace Flow.Launcher.Plugin.Shell
},
new()
{
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_administrator"),
Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_administrator"),
Action = c =>
{
Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, true));
@ -429,10 +429,10 @@ namespace Flow.Launcher.Plugin.Shell
},
new()
{
Title = context.API.GetTranslation("flowlauncher_plugin_cmd_copy"),
Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_copy"),
Action = c =>
{
context.API.CopyToClipboard(selectedResult.Title);
Context.API.CopyToClipboard(selectedResult.Title);
return true;
},
IcoPath = "Images/copy.png",
@ -442,5 +442,10 @@ namespace Flow.Launcher.Plugin.Shell
return results;
}
public void Dispose()
{
Context.API.RemoveGlobalKeyboardCallback(API_GlobalKeyboardEvent);
}
}
}

View file

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

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Windows.Win32;
using Windows.Win32.Foundation;
@ -19,6 +18,8 @@ namespace Flow.Launcher.Plugin.Sys
{
public class Main : IPlugin, ISettingProvider, IPluginI18n
{
private static readonly string ClassName = nameof(Main);
private readonly Dictionary<string, string> KeywordTitleMappings = new()
{
{"Shutdown", "flowlauncher_plugin_sys_shutdown_computer_cmd"},
@ -106,7 +107,7 @@ namespace Flow.Launcher.Plugin.Sys
{
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";
}
@ -117,7 +118,7 @@ namespace Flow.Launcher.Plugin.Sys
{
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";
}

View file

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