mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #3361 from onesounds/250320BookmarkFavicon
Support Svg File Icon Loading & Local favicon for bookmark plugin
This commit is contained in:
commit
2e740b15ef
34 changed files with 642 additions and 240 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ GetModuleHandle
|
|||
GetKeyState
|
||||
VIRTUAL_KEY
|
||||
|
||||
WM_KEYDOWN
|
||||
WM_KEYUP
|
||||
WM_SYSKEYDOWN
|
||||
WM_SYSKEYUP
|
||||
|
||||
EnumWindows
|
||||
|
||||
DwmSetWindowAttribute
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
EnumThreadWindows
|
||||
GetWindowText
|
||||
GetWindowTextLength
|
||||
GetWindowTextLength
|
||||
|
||||
WM_KEYDOWN
|
||||
WM_KEYUP
|
||||
WM_SYSKEYDOWN
|
||||
WM_SYSKEYUP
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue