Merge branch 'dev' into 250411-BlockMultipleKewordsPlugin

# Conflicts:
#	Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs
This commit is contained in:
DB P 2025-04-17 12:37:30 +09:00
commit d97b9ed113
96 changed files with 1421 additions and 658 deletions

View file

@ -1,21 +1,22 @@
using Microsoft.Win32;
using Squirrel;
using System;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using System.Linq;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Win32;
using Squirrel;
namespace Flow.Launcher.Core.Configuration
{
public class Portable : IPortable
{
private static readonly string ClassName = nameof(Portable);
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary>
@ -51,7 +52,7 @@ namespace Flow.Launcher.Core.Configuration
}
catch (Exception e)
{
Log.Exception("|Portable.DisablePortableMode|Error occurred while disabling portable mode", e);
API.LogException(ClassName, "Error occurred while disabling portable mode", e);
}
}
@ -75,7 +76,7 @@ namespace Flow.Launcher.Core.Configuration
}
catch (Exception e)
{
Log.Exception("|Portable.EnablePortableMode|Error occurred while enabling portable mode", e);
API.LogException(ClassName, "Error occurred while enabling portable mode", e);
}
}

View file

@ -1,25 +1,32 @@
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using System;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins
{
public record CommunityPluginSource(string ManifestFileUrl)
{
// 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 static readonly string ClassName = nameof(CommunityPluginSource);
private string latestEtag = "";
private List<UserPlugin> plugins = new();
private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions()
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};
@ -34,35 +41,49 @@ namespace Flow.Launcher.Core.ExternalPlugins
/// </remarks>
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
{
Log.Info(nameof(CommunityPluginSource), $"Loading plugins from {ManifestFileUrl}");
API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
request.Headers.Add("If-None-Match", latestEtag);
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
try
{
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
.ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
this.plugins = await response.Content
.ReadFromJsonAsync<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
.ConfigureAwait(false);
this.latestEtag = response.Headers.ETag?.Tag;
if (response.StatusCode == HttpStatusCode.OK)
{
plugins = await response.Content
.ReadFromJsonAsync<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
.ConfigureAwait(false);
latestEtag = response.Headers.ETag?.Tag;
Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}");
return this.plugins;
API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
return plugins;
}
else if (response.StatusCode == HttpStatusCode.NotModified)
{
API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
return plugins;
}
else
{
API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
return plugins;
}
}
else if (response.StatusCode == HttpStatusCode.NotModified)
catch (Exception e)
{
Log.Info(nameof(CommunityPluginSource), $"Resource {ManifestFileUrl} has not been modified.");
return this.plugins;
}
else
{
Log.Warn(nameof(CommunityPluginSource),
$"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
throw new Exception($"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
{
API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
}
else
{
API.LogException(ClassName, "Error Occurred", e);
}
return plugins;
}
}
}

View file

@ -5,7 +5,6 @@ using System.Linq;
using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -14,6 +13,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
public abstract class AbstractPluginEnvironment
{
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
internal abstract string Language { get; }
@ -120,7 +121,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
else
{
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
Log.Error("PluginsLoader",
API.LogError(ClassName,
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");

View file

@ -4,7 +4,6 @@ using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class JavaScriptEnvironment : TypeScriptEnvironment
{
internal override string Language => AllowedLanguage.JavaScript;

View file

@ -4,7 +4,6 @@ using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class JavaScriptV2Environment : TypeScriptV2Environment
{
internal override string Language => AllowedLanguage.JavaScriptV2;

View file

@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@ -30,13 +31,15 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
// uses Python plugin they need to custom install and use v3.8.9
DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath).Wait();
JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath));
PluginsSettingsFilePath = ExecutablePath;
}

View file

@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
PluginsSettingsFilePath = ExecutablePath;
}

View file

@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
PluginsSettingsFilePath = ExecutablePath;
}

View file

@ -9,6 +9,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
public static class PluginsManifest
{
private static readonly string ClassName = nameof(PluginsManifest);
private static readonly CommunityPluginStore mainPluginStore =
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json",
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
@ -44,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
catch (Exception e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(nameof(PluginsManifest), "Http request failed", e);
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
}
finally
{

View file

@ -3,14 +3,20 @@ using System.Collections.Generic;
using System.Linq;
using System.IO;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using System.Text.Json;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin
{
internal abstract class PluginConfig
{
private static readonly string ClassName = nameof(PluginConfig);
// 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>();
/// <summary>
/// Parse plugin metadata in the given directories
/// </summary>
@ -32,7 +38,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception($"|PluginConfig.ParsePLuginConfigs|Can't delete <{directory}>", e);
API.LogException(ClassName, $"Can't delete <{directory}>", e);
}
}
else
@ -49,11 +55,11 @@ namespace Flow.Launcher.Core.Plugin
duplicateList
.ForEach(
x => Log.Warn("PluginConfig",
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
"not loaded due to version not the highest of the duplicates",
x.Name, x.ID, x.Version),
"GetUniqueLatestPluginMetadata"));
x => API.LogWarn(ClassName,
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
"not loaded due to version not the highest of the duplicates",
x.Name, x.ID, x.Version),
"GetUniqueLatestPluginMetadata"));
return uniqueList;
}
@ -101,7 +107,7 @@ namespace Flow.Launcher.Core.Plugin
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
if (!File.Exists(configPath))
{
Log.Error($"|PluginConfig.GetPluginMetadata|Didn't find config file <{configPath}>");
API.LogError(ClassName, $"Didn't find config file <{configPath}>");
return null;
}
@ -117,19 +123,19 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception($"|PluginConfig.GetPluginMetadata|invalid json for config <{configPath}>", e);
API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
return null;
}
if (!AllowedLanguage.IsAllowed(metadata.Language))
{
Log.Error($"|PluginConfig.GetPluginMetadata|Invalid language <{metadata.Language}> for config <{configPath}>");
API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
return null;
}
if (!File.Exists(metadata.ExecuteFilePath))
{
Log.Error($"|PluginConfig.GetPluginMetadata|execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
return null;
}

View file

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -37,7 +36,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new();
private static readonly List<string> _modifiedPlugins = new();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
@ -61,10 +60,17 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public static void Save()
{
foreach (var plugin in AllPlugins)
foreach (var pluginPair in AllPlugins)
{
var savable = plugin.Plugin as ISavable;
savable?.Save();
var savable = pluginPair.Plugin as ISavable;
try
{
savable?.Save();
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
}
API.SavePluginSettings();
@ -81,14 +87,21 @@ namespace Flow.Launcher.Core.Plugin
private static async Task DisposePluginAsync(PluginPair pluginPair)
{
switch (pluginPair.Plugin)
try
{
case IDisposable disposable:
disposable.Dispose();
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
switch (pluginPair.Plugin)
{
case IDisposable disposable:
disposable.Dispose();
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
}
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
}
}
@ -200,12 +213,12 @@ namespace Flow.Launcher.Core.Plugin
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
pair.Metadata.InitTime += milliseconds;
Log.Info(
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
API.LogInfo(ClassName,
$"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
}
catch (Exception e)
{
Log.Exception(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
pair.Metadata.Disabled = true;
failedPlugins.Enqueue(pair);
}
@ -292,7 +305,7 @@ namespace Flow.Launcher.Core.Plugin
{
Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info",
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
@ -356,8 +369,8 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception(
$"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
API.LogException(ClassName,
$"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
e);
}
}
@ -369,8 +382,8 @@ namespace Flow.Launcher.Core.Plugin
{
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
}
/// <summary>
@ -549,7 +562,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e);
API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
}
if (checkModified)
@ -594,7 +607,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e);
API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
}
@ -610,7 +623,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e);
API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"),
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
}

View file

@ -89,19 +89,19 @@ namespace Flow.Launcher.Core.Plugin
#else
catch (Exception e) when (assembly == null)
{
Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e);
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif

View file

@ -1,21 +1,19 @@
#nullable enable
using System;
using System.Collections.Generic;
using System;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using Meziantou.Framework.Win32;
using Microsoft.VisualBasic.ApplicationServices;
using Nerdbank.Streams;
#nullable enable
namespace Flow.Launcher.Core.Plugin
{
internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2
{
private static JobObject _jobObject = new JobObject();
private static readonly JobObject _jobObject = new();
static ProcessStreamPluginV2()
{
@ -66,11 +64,10 @@ namespace Flow.Launcher.Core.Plugin
ClientPipe = new DuplexPipe(reader, writer);
}
public override async Task ReloadDataAsync()
{
var oldProcess = ClientProcess;
ClientProcess = Process.Start(StartInfo);
ClientProcess = Process.Start(StartInfo)!;
ArgumentNullException.ThrowIfNull(ClientProcess);
SetupPipe(ClientProcess);
await base.ReloadDataAsync();
@ -79,7 +76,6 @@ namespace Flow.Launcher.Core.Plugin
oldProcess.Dispose();
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();

View file

@ -6,7 +6,6 @@ using System.Reflection;
using System.Windows;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using System.Globalization;
@ -17,13 +16,19 @@ namespace Flow.Launcher.Core.Resource
{
public class Internationalization
{
private static readonly string ClassName = nameof(Internationalization);
// 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 const string Folder = "Languages";
private const string DefaultLanguageCode = "en";
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
private readonly Settings _settings;
private readonly List<string> _languageDirectories = new List<string>();
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
private readonly List<string> _languageDirectories = new();
private readonly List<ResourceDictionary> _oldResources = new();
private readonly string SystemLanguageCode;
public Internationalization(Settings settings)
@ -80,7 +85,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
API.LogError(ClassName, $"Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
}
}
@ -144,13 +149,13 @@ namespace Flow.Launcher.Core.Resource
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
}
private Language GetLanguageByLanguageCode(string languageCode)
private static Language GetLanguageByLanguageCode(string languageCode)
{
var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
if (language == null)
{
Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>");
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
return AvailableLanguages.English;
}
else
@ -239,7 +244,7 @@ namespace Flow.Launcher.Core.Resource
return list;
}
public string GetTranslation(string key)
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
@ -248,7 +253,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
Log.Error($"|Internationalization.GetTranslation|No Translation for key {key}");
API.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
@ -257,8 +262,7 @@ namespace Flow.Launcher.Core.Resource
{
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var pluginI18N = p.Plugin as IPluginI18n;
if (pluginI18N == null) return;
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
@ -267,31 +271,31 @@ namespace Flow.Launcher.Core.Resource
}
catch (Exception e)
{
Log.Exception($"|Internationalization.UpdatePluginMetadataTranslations|Failed for <{p.Metadata.Name}>", e);
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
public string LanguageFile(string folder, string language)
private static string LanguageFile(string folder, string language)
{
if (Directory.Exists(folder))
{
string path = Path.Combine(folder, language);
var path = Path.Combine(folder, language);
if (File.Exists(path))
{
return path;
}
else
{
Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>");
string english = Path.Combine(folder, DefaultFile);
API.LogError(ClassName, $"Language path can't be found <{path}>");
var english = Path.Combine(folder, DefaultFile);
if (File.Exists(english))
{
return english;
}
else
{
Log.Error($"|Internationalization.LanguageFile|Default English Language path can't be found <{path}>");
API.LogError(ClassName, $"Default English Language path can't be found <{path}>");
return string.Empty;
}
}

View file

@ -13,7 +13,6 @@ using System.Windows.Media.Effects;
using System.Windows.Shell;
using System.Windows.Threading;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -25,6 +24,8 @@ namespace Flow.Launcher.Core.Resource
{
#region Properties & Fields
private readonly string ClassName = nameof(Theme);
public bool BlurEnabled { get; private set; }
private const string ThemeMetadataNamePrefix = "Name:";
@ -73,7 +74,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
Log.Error("Current theme resource not found. Initializing with default theme.");
_api.LogError(ClassName, "Current theme resource not found. Initializing with default theme.");
_oldTheme = Constant.DefaultTheme;
};
}
@ -92,7 +93,7 @@ namespace Flow.Launcher.Core.Resource
}
catch (Exception e)
{
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
_api.LogException(ClassName, $"Exception when create directory <{dir}>", e);
}
}
}
@ -135,7 +136,7 @@ namespace Flow.Launcher.Core.Resource
}
catch (Exception e)
{
Log.Exception("Error occurred while updating theme fonts", e);
_api.LogException(ClassName, "Error occurred while updating theme fonts", e);
}
}
@ -387,7 +388,7 @@ namespace Flow.Launcher.Core.Resource
var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme);
if (matchingTheme == null)
{
Log.Warn($"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme.");
_api.LogWarn(ClassName, $"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme.");
}
return matchingTheme ?? themes.FirstOrDefault();
}
@ -440,7 +441,7 @@ namespace Flow.Launcher.Core.Resource
}
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
_api.LogError(ClassName, $"Theme <{theme}> path can't be found");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
@ -450,7 +451,7 @@ namespace Flow.Launcher.Core.Resource
}
catch (XamlParseException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));

View file

@ -12,7 +12,6 @@ using System.Windows;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using JetBrains.Annotations;
@ -24,6 +23,8 @@ namespace Flow.Launcher.Core
{
public string GitHubRepository { get; init; }
private static readonly string ClassName = nameof(Updater);
private readonly IPublicAPI _api;
public Updater(IPublicAPI publicAPI, string gitHubRepository)
@ -51,7 +52,7 @@ namespace Flow.Launcher.Core
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version);
Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
_api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
if (newReleaseVersion <= currentVersion)
{
@ -84,7 +85,7 @@ namespace Flow.Launcher.Core
var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
@ -93,10 +94,14 @@ namespace Flow.Launcher.Core
}
catch (Exception e)
{
if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException))
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
{
_api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
}
else
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
{
_api.LogException(ClassName, $"Error Occurred", e);
}
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),

View file

@ -1,30 +1,31 @@
using System.IO;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using System.Threading;
using Flow.Launcher.Plugin;
using CommunityToolkit.Mvvm.DependencyInjection;
using JetBrains.Annotations;
namespace Flow.Launcher.Infrastructure.Http
{
public static class Http
{
private static readonly string ClassName = nameof(Http);
private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko";
private static HttpClient client = new HttpClient();
private static readonly HttpClient client = new();
static Http()
{
// need to be added so it would work on a win10 machine
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
| SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
client.DefaultRequestHeaders.Add("User-Agent", UserAgent);
HttpClient.DefaultProxy = WebProxy;
@ -34,7 +35,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static HttpProxy Proxy
{
private get { return proxy; }
private get => proxy;
set
{
proxy = value;
@ -72,13 +73,13 @@ namespace Flow.Launcher.Infrastructure.Http
ProxyProperty.Port => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), WebProxy.Credentials),
ProxyProperty.UserName => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)),
ProxyProperty.Password => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)),
_ => throw new ArgumentOutOfRangeException()
_ => throw new ArgumentOutOfRangeException(null)
};
}
catch (UriFormatException e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
Log.Exception(ClassName, "Unable to parse Uri", e);
}
}
@ -134,7 +135,7 @@ namespace Flow.Launcher.Infrastructure.Http
}
catch (HttpRequestException e)
{
Log.Exception("Infrastructure.Http", "Http Request Error", e, "DownloadAsync");
Log.Exception(ClassName, "Http Request Error", e, "DownloadAsync");
throw;
}
}
@ -147,7 +148,7 @@ namespace Flow.Launcher.Infrastructure.Http
/// <returns>The Http result as string. Null if cancellation requested</returns>
public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
Log.Debug(ClassName, $"Url <{url}>");
return GetAsync(new Uri(url), token);
}
@ -159,7 +160,7 @@ namespace Flow.Launcher.Infrastructure.Http
/// <returns>The Http result as string. Null if cancellation requested</returns>
public static async Task<string> GetAsync([NotNull] Uri url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
Log.Debug(ClassName, $"Url <{url}>");
using var response = await client.GetAsync(url, token);
var content = await response.Content.ReadAsStringAsync(token);
if (response.StatusCode != HttpStatusCode.OK)
@ -191,7 +192,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static async Task<Stream> GetStreamAsync([NotNull] Uri url,
CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
Log.Debug(ClassName, $"Url <{url}>");
return await client.GetStreamAsync(url, token);
}
@ -202,7 +203,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static async Task<HttpResponseMessage> GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
Log.Debug(ClassName, $"Url <{url}>");
return await client.GetAsync(url, completionOption, token);
}

View file

@ -7,9 +7,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;
@ -17,10 +16,6 @@ namespace Flow.Launcher.Infrastructure.Image
{
public static class ImageLoader
{
// 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 static readonly string ClassName = nameof(ImageLoader);
private static readonly ImageCache ImageCache = new();
@ -58,14 +53,14 @@ namespace Flow.Launcher.Infrastructure.Image
_ = Task.Run(async () =>
{
await API.StopwatchLogInfoAsync(ClassName, "Preload images cost", async () =>
await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () =>
{
foreach (var (path, isFullImage) in usage)
{
await LoadAsync(path, isFullImage);
}
});
API.LogInfo(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
Log.Info(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
});
}
@ -81,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Image
}
catch (System.Exception e)
{
API.LogException(ClassName, "Failed to save image cache to file", e);
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
finally
{
@ -176,8 +171,8 @@ namespace Flow.Launcher.Infrastructure.Image
}
catch (System.Exception e2)
{
API.LogException(ClassName, $"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
API.LogException(ClassName, $"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e);
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageCache[path, false] = image;
@ -243,7 +238,7 @@ namespace Flow.Launcher.Infrastructure.Image
{
image = Image;
type = ImageType.Error;
API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
Log.Exception(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
}
}
else
@ -267,7 +262,7 @@ namespace Flow.Launcher.Infrastructure.Image
{
image = Image;
type = ImageType.Error;
API.LogException(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
Log.Exception(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
}
}
else

View file

@ -12,7 +12,7 @@ using Windows.Win32.Graphics.Gdi;
namespace Flow.Launcher.Infrastructure.Image
{
/// <summary>
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
/// Subclass of <see cref="SIIGBF"/>
/// </summary>
[Flags]
public enum ThumbnailOptions
@ -31,7 +31,9 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
private static readonly HRESULT S_EXTRACTIONFAILED = (HRESULT)0x8004B200;
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
@ -79,9 +81,10 @@ namespace Flow.Launcher.Infrastructure.Image
{
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
}
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
catch (COMException ex) when (options == ThumbnailOptions.ThumbnailOnly &&
(ex.HResult == S_PATHNOTFOUND || ex.HResult == S_EXTRACTIONFAILED))
{
// Fallback to IconOnly if ThumbnailOnly fails
// Fallback to IconOnly if extraction fails or files cannot be found
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
}
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
@ -89,6 +92,11 @@ namespace Flow.Launcher.Infrastructure.Image
// Fallback to IconOnly if files cannot be found
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
}
catch (System.Exception ex)
{
// Handle other exceptions
throw new InvalidOperationException("Failed to get thumbnail", ex);
}
}
finally
{

View file

@ -94,13 +94,6 @@ namespace Flow.Launcher.Infrastructure.Logger
logger.Fatal(message);
}
private static bool FormatValid(string message)
{
var parts = message.Split('|');
var valid = parts.Length == 3 && !string.IsNullOrWhiteSpace(parts[1]) && !string.IsNullOrWhiteSpace(parts[2]);
return valid;
}
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
{
exception = exception.Demystify();
@ -135,57 +128,14 @@ namespace Flow.Launcher.Infrastructure.Logger
return className;
}
#if !DEBUG
private static void ExceptionInternal(string classAndMethod, string message, System.Exception e)
{
var logger = LogManager.GetLogger(classAndMethod);
logger.Error(e, message);
}
private static void LogInternal(string message, LogLevel level)
{
if (FormatValid(message))
{
var parts = message.Split('|');
var prefix = parts[1];
var unprefixed = parts[2];
var logger = LogManager.GetLogger(prefix);
logger.Log(level, unprefixed);
}
else
{
LogFaultyFormat(message);
}
}
/// Example: "|ClassName.MethodName|Message"
/// <param name="message">Example: "|ClassName.MethodName|Message" </param>
/// <param name="e">Exception</param>
public static void Exception(string message, System.Exception e)
{
e = e.Demystify();
#if DEBUG
ExceptionDispatchInfo.Capture(e).Throw();
#else
if (FormatValid(message))
{
var parts = message.Split('|');
var prefix = parts[1];
var unprefixed = parts[2];
ExceptionInternal(prefix, unprefixed, e);
}
else
{
LogFaultyFormat(message);
}
#endif
}
/// Example: "|ClassName.MethodName|Message"
public static void Error(string message)
{
LogInternal(message, LogLevel.Error);
}
public static void Error(string className, string message, [CallerMemberName] string methodName = "")
{
@ -206,33 +156,15 @@ namespace Flow.Launcher.Infrastructure.Logger
LogInternal(LogLevel.Debug, className, message, methodName);
}
/// Example: "|ClassName.MethodName|Message""
public static void Debug(string message)
{
LogInternal(message, LogLevel.Debug);
}
public static void Info(string className, string message, [CallerMemberName] string methodName = "")
{
LogInternal(LogLevel.Info, className, message, methodName);
}
/// Example: "|ClassName.MethodName|Message"
public static void Info(string message)
{
LogInternal(message, LogLevel.Info);
}
public static void Warn(string className, string message, [CallerMemberName] string methodName = "")
{
LogInternal(LogLevel.Warn, className, message, methodName);
}
/// Example: "|ClassName.MethodName|Message"
public static void Warn(string message)
{
LogInternal(message, LogLevel.Warn);
}
}
public enum LOGLEVEL

View file

@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
@ -9,54 +10,50 @@ namespace Flow.Launcher.Infrastructure
/// <summary>
/// This stopwatch will appear only in Debug mode
/// </summary>
public static long Debug(string message, Action action)
public static long Debug(string className, string message, Action action, [CallerMemberName] string methodName = "")
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
action();
stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>";
Log.Debug(info);
Log.Debug(className, $"{message} <{milliseconds}ms>", methodName);
return milliseconds;
}
/// <summary>
/// This stopwatch will appear only in Debug mode
/// </summary>
public static async Task<long> DebugAsync(string message, Func<Task> action)
public static async Task<long> DebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "")
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
await action();
stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>";
Log.Debug(info);
Log.Debug(className, $"{message} <{milliseconds}ms>", methodName);
return milliseconds;
}
public static long Normal(string message, Action action)
public static long Info(string className, string message, Action action, [CallerMemberName] string methodName = "")
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
action();
stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>";
Log.Info(info);
Log.Info(className, $"{message} <{milliseconds}ms>", methodName);
return milliseconds;
}
public static async Task<long> NormalAsync(string message, Func<Task> action)
public static async Task<long> InfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "")
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
await action();
stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>";
Log.Info(info);
Log.Info(className, $"{message} <{milliseconds}ms>", methodName);
return milliseconds;
}
}

View file

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
@ -19,6 +20,8 @@ namespace Flow.Launcher.Infrastructure.Storage
/// </remarks>
public class BinaryStorage<T> : ISavable
{
private static readonly string ClassName = "BinaryStorage";
protected T? Data;
public const string FileSuffix = ".cache";
@ -40,6 +43,16 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
// Let the old Program plugin get this constructor
[Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")]
public BinaryStorage(string filename, string directoryPath = null!)
{
DirectoryPath = directoryPath ?? DataLocation.CacheDirectory;
FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public async ValueTask<T> TryLoadAsync(T defaultData)
{
if (Data != null) return Data;
@ -48,7 +61,7 @@ namespace Flow.Launcher.Infrastructure.Storage
{
if (new FileInfo(FilePath).Length == 0)
{
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
Data = defaultData;
await SaveAsync();
}
@ -58,7 +71,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
else
{
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
Log.Info(ClassName, "Cache file not exist, load default data");
Data = defaultData;
await SaveAsync();
}
@ -82,8 +95,10 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
var serialized = MemoryPackSerializer.Serialize(Data);
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = MemoryPackSerializer.Serialize(Data);
File.WriteAllBytes(FilePath, serialized);
}
@ -103,6 +118,9 @@ namespace Flow.Launcher.Infrastructure.Storage
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data)
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
await using var stream = new FileStream(FilePath, FileMode.Create);
await MemoryPackSerializer.SerializeAsync(stream, data);
}

View file

@ -1,8 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
@ -11,17 +10,13 @@ namespace Flow.Launcher.Infrastructure.Storage
{
private static readonly string ClassName = "FlowLauncherJsonStorage";
// 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 FlowLauncherJsonStorage()
{
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
FilesFolders.ValidateDirectory(directoryPath);
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
FilesFolders.ValidateDirectory(DirectoryPath);
var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public new void Save()
@ -32,7 +27,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
}
}
@ -44,7 +39,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
}
}
}

View file

@ -16,6 +16,8 @@ namespace Flow.Launcher.Infrastructure.Storage
/// </summary>
public class JsonStorage<T> : ISavable where T : new()
{
private static readonly string ClassName = "JsonStorage";
protected T? Data;
// need a new directory name
@ -104,7 +106,7 @@ namespace Flow.Launcher.Infrastructure.Storage
private void RestoreBackup()
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
Log.Info(ClassName, $"Failed to load settings.json, {BackupFilePath} restored successfully");
if (File.Exists(FilePath))
File.Replace(BackupFilePath, FilePath, null);
@ -183,7 +185,10 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
string serialized = JsonSerializer.Serialize(Data,
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = JsonSerializer.Serialize(Data,
new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(TempFilePath, serialized);
@ -193,6 +198,9 @@ namespace Flow.Launcher.Infrastructure.Storage
public async Task SaveAsync()
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
await using var tempOutput = File.OpenWrite(TempFilePath);
await JsonSerializer.SerializeAsync(tempOutput, Data,
new JsonSerializerOptions { WriteIndented = true });

View file

@ -1,7 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
@ -10,10 +9,6 @@ namespace Flow.Launcher.Infrastructure.Storage
{
private static readonly string ClassName = "PluginBinaryStorage";
// 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 PluginBinaryStorage(string cacheName, string cacheDirectory)
{
DirectoryPath = cacheDirectory;
@ -30,7 +25,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
@ -42,7 +37,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
}

View file

@ -1,8 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
@ -14,10 +13,6 @@ namespace Flow.Launcher.Infrastructure.Storage
private static readonly string ClassName = "PluginJsonStorage";
// 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 PluginJsonStorage()
{
// C# related, add python related below
@ -42,7 +37,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
}
}
@ -54,7 +49,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
}
}
}

View file

@ -101,6 +101,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool UseAnimation { get; set; } = true;
public bool UseSound { get; set; } = true;
public double SoundVolume { get; set; } = 50;
public bool ShowBadges { get; set; } = false;
public bool ShowBadgesGlobalOnly { get; set; } = false;
public bool UseClock { get; set; } = true;
public bool UseDate { get; set; } = false;

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows;
@ -364,20 +365,10 @@ namespace Flow.Launcher.Infrastructure
// No installed English layout found
if (enHKL == HKL.Null) return;
// When application is exiting, the Application.Current will be null
if (Application.Current == null) return;
// Get the FL main window
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
// Get the foreground window
var hwnd = PInvoke.GetForegroundWindow();
if (hwnd == HWND.Null) return;
// Check if the FL main window is the current foreground window
if (!IsForegroundWindow(hwnd))
{
var result = PInvoke.SetForegroundWindow(hwnd);
if (!result) throw new Win32Exception(Marshal.GetLastWin32Error());
}
// Get the current foreground window thread ID
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
@ -517,5 +508,92 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
#region Korean IME
public static bool IsWindows11()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Environment.OSVersion.Version.Build >= 22000;
}
public static bool IsKoreanIMEExist()
{
return GetLegacyKoreanIMERegistryValue() != null;
}
public static bool IsLegacyKoreanIMEEnabled()
{
object value = GetLegacyKoreanIMERegistryValue();
if (value is int intValue)
{
return intValue == 1;
}
else if (value != null && int.TryParse(value.ToString(), out int parsedValue))
{
return parsedValue == 1;
}
return false;
}
public static bool SetLegacyKoreanIMEEnabled(bool enable)
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}";
const string valueName = "NoTsf3Override5";
try
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(subKeyPath);
if (key != null)
{
int value = enable ? 1 : 0;
key.SetValue(valueName, value, RegistryValueKind.DWord);
return true;
}
}
catch (System.Exception)
{
// Ignored
}
return false;
}
public static object GetLegacyKoreanIMERegistryValue()
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}";
const string valueName = "NoTsf3Override5";
try
{
using RegistryKey key = Registry.CurrentUser.OpenSubKey(subKeyPath);
if (key != null)
{
return key.GetValue(valueName);
}
}
catch (System.Exception)
{
// Ignored
}
return null;
}
public static void OpenImeSettings()
{
try
{
Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true });
}
catch (System.Exception)
{
// Ignored
}
}
#endregion
}
}

View file

@ -12,12 +12,19 @@ namespace Flow.Launcher.Plugin
/// </summary>
public class Result
{
/// <summary>
/// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
/// </summary>
public const int MaxScore = int.MaxValue;
private string _pluginDirectory;
private string _icoPath;
private string _copyText = string.Empty;
private string _badgeIcoPath;
/// <summary>
/// The title of the result. This is always required.
/// </summary>
@ -60,7 +67,7 @@ namespace Flow.Launcher.Plugin
/// <remarks>GlyphInfo is prioritized if not null</remarks>
public string IcoPath
{
get { return _icoPath; }
get => _icoPath;
set
{
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
@ -80,6 +87,33 @@ namespace Flow.Launcher.Plugin
}
}
/// <summary>
/// The image to be displayed for the badge of the result.
/// </summary>
/// <value>Can be a local file path or a URL.</value>
/// <remarks>If null or empty, will use plugin icon</remarks>
public string BadgeIcoPath
{
get => _badgeIcoPath;
set
{
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
if (!string.IsNullOrEmpty(value)
&& !string.IsNullOrEmpty(PluginDirectory)
&& !Path.IsPathRooted(value)
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
_badgeIcoPath = Path.Combine(PluginDirectory, value);
}
else
{
_badgeIcoPath = value;
}
}
}
/// <summary>
/// Determines if Icon has a border radius
/// </summary>
@ -94,14 +128,18 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Delegate to load an icon for this result.
/// </summary>
public IconDelegate Icon;
public IconDelegate Icon = null;
/// <summary>
/// Delegate to load an icon for the badge of this result.
/// </summary>
public IconDelegate BadgeIcon = null;
/// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
/// </summary>
public GlyphInfo Glyph { get; init; }
/// <summary>
/// An action to take in the form of a function call when the result has been selected.
/// </summary>
@ -143,59 +181,19 @@ namespace Flow.Launcher.Plugin
/// </summary>
public string PluginDirectory
{
get { return _pluginDirectory; }
get => _pluginDirectory;
set
{
_pluginDirectory = value;
// When the Result object is returned from the query call, PluginDirectory is not provided until
// UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available
// we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded.
// we need to update (only if not Uri path) the IcoPath and BadgeIcoPath with the full absolute path so the image can be loaded.
IcoPath = _icoPath;
BadgeIcoPath = _badgeIcoPath;
}
}
/// <inheritdoc />
public override string ToString()
{
return Title + SubTitle + Score;
}
/// <summary>
/// Clones the current result
/// </summary>
public Result Clone()
{
return new Result
{
Title = Title,
SubTitle = SubTitle,
ActionKeywordAssigned = ActionKeywordAssigned,
CopyText = CopyText,
AutoCompleteText = AutoCompleteText,
IcoPath = IcoPath,
RoundedIcon = RoundedIcon,
Icon = Icon,
Glyph = Glyph,
Action = Action,
AsyncAction = AsyncAction,
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey
};
}
/// <summary>
/// Additional data associated with this result
/// </summary>
@ -224,16 +222,6 @@ namespace Flow.Launcher.Plugin
/// </summary>
public Lazy<UserControl> PreviewPanel { get; set; }
/// <summary>
/// Run this result, asynchronously
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public ValueTask<bool> ExecuteAsync(ActionContext context)
{
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
}
/// <summary>
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
/// </summary>
@ -255,11 +243,6 @@ namespace Flow.Launcher.Plugin
/// </summary>
public bool AddSelectedCount { get; set; } = true;
/// <summary>
/// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
/// </summary>
public const int MaxScore = int.MaxValue;
/// <summary>
/// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
/// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
@ -268,6 +251,66 @@ namespace Flow.Launcher.Plugin
/// </summary>
public string RecordKey { get; set; } = null;
/// <summary>
/// Determines if the badge icon should be shown.
/// If users want to show the result badges and here you set this to true, the results will show the badge icon.
/// </summary>
public bool ShowBadge { get; set; } = false;
/// <summary>
/// Run this result, asynchronously
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public ValueTask<bool> ExecuteAsync(ActionContext context)
{
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
}
/// <inheritdoc />
public override string ToString()
{
return Title + SubTitle + Score;
}
/// <summary>
/// Clones the current result
/// </summary>
public Result Clone()
{
return new Result
{
Title = Title,
SubTitle = SubTitle,
ActionKeywordAssigned = ActionKeywordAssigned,
CopyText = CopyText,
AutoCompleteText = AutoCompleteText,
IcoPath = IcoPath,
BadgeIcoPath = BadgeIcoPath,
RoundedIcon = RoundedIcon,
Icon = Icon,
BadgeIcon = BadgeIcon,
Glyph = Glyph,
Action = Action,
AsyncAction = AsyncAction,
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
};
}
/// <summary>
/// Info of the preview section of a <see cref="Result"/>
/// </summary>

View file

@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands
var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1))
{
string previousDirectoryPath = path.Substring(0, index + 1);
return locationExists(previousDirectoryPath) ? previousDirectoryPath : "";
string previousDirectoryPath = path[..(index + 1)];
return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty;
}
else
{
return "";
return string.Empty;
}
}
@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
// not full path, get previous level directory string
var indexOfSeparator = path.LastIndexOf('\\');
return path.Substring(0, indexOfSeparator + 1);
return path[..(indexOfSeparator + 1)];
}
return path;

View file

@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins
}
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
[TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
[TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
{
// Given
@ -59,7 +59,7 @@ namespace Flow.Launcher.Test.Plugins
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
" ORDER BY System.FileName")]
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
string folderPath, string userSearchString, string expectedString)
{
@ -87,8 +87,8 @@ namespace Flow.Launcher.Test.Plugins
[SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
[TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
$"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@ -107,7 +107,6 @@ namespace Flow.Launcher.Test.Plugins
ClassicAssert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
@ -127,7 +126,7 @@ namespace Flow.Launcher.Test.Plugins
[SupportedOSPlatform("windows7.0")]
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
$"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{

View file

@ -148,11 +148,12 @@ namespace Flow.Launcher
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
API.LogInfo(ClassName, "Runtime info:{ErrorReporting.RuntimeInfo()}");
RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync();
@ -175,7 +176,7 @@ namespace Flow.Launcher
_mainWindow = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
API.LogInfo(ClassName, "Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher;
@ -191,7 +192,7 @@ namespace Flow.Launcher
AutoUpdates();
API.SaveAppAllSettings();
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
});
}
@ -249,19 +250,19 @@ namespace Flow.Launcher
{
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
{
Log.Info("|App.RegisterExitEvents|Process Exit");
API.LogInfo(ClassName, "Process Exit");
Dispose();
};
Current.Exit += (s, e) =>
{
Log.Info("|App.RegisterExitEvents|Application Exit");
API.LogInfo(ClassName, "Application Exit");
Dispose();
};
Current.SessionEnding += (s, e) =>
{
Log.Info("|App.RegisterExitEvents|Session Ending");
API.LogInfo(ClassName, "Session Ending");
Dispose();
};
}
@ -284,6 +285,15 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
}
/// <summary>
/// let exception throw as normal is better for Debug
/// </summary>
[Conditional("RELEASE")]
private static void RegisterTaskSchedulerUnhandledException()
{
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
}
#endregion
#region IDisposable
@ -316,7 +326,7 @@ namespace Flow.Launcher
API.StopwatchLogInfo(ClassName, "Dispose cost", () =>
{
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
API.LogInfo(ClassName, "Begin Flow Launcher dispose ----------------------------------------------------");
if (disposing)
{
@ -326,7 +336,7 @@ namespace Flow.Launcher
_mainVM?.Dispose();
}
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");
});
}

View file

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class BadgePositionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double actualWidth && parameter is string param)
{
double offset = actualWidth / 2 - 8;
if (param == "1") // X-Offset
{
return offset + 2;
}
else if (param == "2") // Y-Offset
{
return offset + 2;
}
}
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

View file

@ -4,13 +4,14 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Converters;
public class QuerySuggestionBoxConverter : IMultiValueConverter
{
private static readonly string ClassName = nameof(QuerySuggestionBoxConverter);
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// values[0] is TextBox: The textbox displaying the autocomplete suggestion
@ -64,7 +65,7 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
}
catch (Exception e)
{
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
App.API.LogException(ClassName, "fail to convert text for suggestion box", e);
return string.Empty;
}
}

View file

@ -0,0 +1,27 @@
using System.Windows.Data;
using System;
using System.Globalization;
using System.Windows;
namespace Flow.Launcher.Converters;
public class SizeRatioConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double size && parameter is string ratioString)
{
if (double.TryParse(ratioString, NumberStyles.Any, CultureInfo.InvariantCulture, out double ratio))
{
return size * ratio;
}
}
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Helper;
public class AutoStartup
{
private static readonly string ClassName = nameof(AutoStartup);
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
@ -34,7 +36,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
App.API.LogError(ClassName, $"Ignoring non-critical registry error (querying if enabled): {e}");
}
return false;
@ -61,7 +63,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Failed to check logon task: {e}");
App.API.LogError(ClassName, $"Failed to check logon task: {e}");
}
}
@ -112,7 +114,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
App.API.LogError(ClassName, $"Failed to disable auto-startup: {e}");
throw;
}
}
@ -133,7 +135,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
App.API.LogError(ClassName, $"Failed to enable auto-startup: {e}");
throw;
}
}
@ -161,7 +163,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Failed to schedule logon task: {e}");
App.API.LogError(ClassName, $"Failed to schedule logon task: {e}");
return false;
}
}
@ -176,7 +178,7 @@ public class AutoStartup
}
catch (Exception e)
{
Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}");
App.API.LogError(ClassName, $"Failed to unschedule logon task: {e}");
return false;
}
}

View file

@ -1,8 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using NLog;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception;
using NLog;
namespace Flow.Launcher.Helper;
@ -30,6 +32,13 @@ public static class ErrorReporting
e.Handled = true;
}
public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
//handle unobserved task exceptions
Application.Current.Dispatcher.Invoke(() => Report(e.Exception));
//prevent application exit, so the user can copy the prompted error info
}
public static string RuntimeInfo()
{
var info =

View file

@ -12,6 +12,8 @@ namespace Flow.Launcher.Helper;
internal static class HotKeyMapper
{
private static readonly string ClassName = nameof(HotKeyMapper);
private static Settings _settings;
private static MainViewModel _mainViewModel;
@ -51,7 +53,7 @@ internal static class HotKeyMapper
}
catch (Exception e)
{
Log.Error(
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
@ -76,7 +78,7 @@ internal static class HotKeyMapper
}
catch (Exception e)
{
Log.Error(
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace,
@ -102,7 +104,7 @@ internal static class HotKeyMapper
}
catch (Exception e)
{
Log.Error(
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));

View file

@ -12,6 +12,8 @@ namespace Flow.Launcher.Helper;
public static class WallpaperPathRetrieval
{
private static readonly string ClassName = nameof(WallpaperPathRetrieval);
private const int MaxCacheSize = 3;
private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new();
private static readonly object CacheLock = new();
@ -29,7 +31,7 @@ public static class WallpaperPathRetrieval
var wallpaperPath = Win32Helper.GetWallpaperPath();
if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath))
{
App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}");
App.API.LogInfo(ClassName, $"Wallpaper path is invalid: {wallpaperPath}");
var wallpaperColor = GetWallpaperColor();
return new SolidColorBrush(wallpaperColor);
}
@ -54,7 +56,7 @@ public static class WallpaperPathRetrieval
if (originalWidth == 0 || originalHeight == 0)
{
App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}");
App.API.LogInfo(ClassName, $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}");
return new SolidColorBrush(Colors.Transparent);
}
@ -95,7 +97,7 @@ public static class WallpaperPathRetrieval
}
catch (Exception ex)
{
App.API.LogException(nameof(WallpaperPathRetrieval), "Error retrieving wallpaper", ex);
App.API.LogException(ClassName, "Error retrieving wallpaper", ex);
return new SolidColorBrush(Colors.Transparent);
}
}
@ -113,7 +115,7 @@ public static class WallpaperPathRetrieval
}
catch (Exception ex)
{
App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex);
App.API.LogException(ClassName, "Error parsing wallpaper color", ex);
}
}

View file

@ -1,6 +1,4 @@
#nullable enable
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@ -9,6 +7,8 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
#nullable enable
namespace Flow.Launcher
{
public partial class HotkeyControl
@ -242,7 +242,11 @@ namespace Flow.Launcher
HotKeyMapper.RemoveHotkey(Hotkey);
}
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle);
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle)
{
Owner = Window.GetWindow(this)
};
await dialog.ShowAsync();
switch (dialog.ResultType)
{

View file

@ -113,6 +113,19 @@
<system:String x:Key="searchDelayNumberBoxToolTip">Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Wait time before showing results after typing stops. Higher values wait longer. (ms)</system:String>
<system:String x:Key="KoreanImeTitle">Information for Korean IME user</system:String>
<system:String x:Key="KoreanImeGuide">
The Korean input method used in Windows 11 may cause some issues in Flow Launcher.&#x0a;
If you experience any problems, you may need to enable "Use previous version of Korean IME".&#x0a;&#x0a;
Open Setting in Windows 11 and go to:&#x0a;
Time &amp; Language &gt; Language &amp; Region &gt; Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility,&#x0a;
and enable "Use previous version of Microsoft IME".&#x0a;&#x0a;
</system:String>
<system:String x:Key="KoreanImeOpenLink">Open Language and Region System Settings</system:String>
<system:String x:Key="KoreanImeOpenLinkToolTip">Opens the Korean IME setting location. Go to Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility</system:String>
<system:String x:Key="KoreanImeOpenLinkButton">Open</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -285,6 +298,10 @@
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String>
<system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String>
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">Show badges for query results where supported</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>

View file

@ -291,15 +291,15 @@ namespace Flow.Launcher
{
if (!CanClose)
{
CanClose = true;
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await ImageLoader.WaitSaveAsync();
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we can close the main window
CanClose = true;
// Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
// After plugins are all disposed, we shutdown application to close app
// We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown();
}
}

View file

@ -4,13 +4,13 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher
{
public partial class MessageBoxEx : Window
{
private static readonly string ClassName = nameof(MessageBoxEx);
private static MessageBoxEx msgBox;
private static MessageBoxResult _result = MessageBoxResult.None;
@ -59,7 +59,7 @@ namespace Flow.Launcher
}
catch (Exception e)
{
Log.Error($"|MessageBoxEx.Show|An error occurred: {e.Message}");
App.API.LogError(ClassName, $"An error occurred: {e.Message}");
msgBox = null;
return MessageBoxResult.None;
}

View file

@ -5,7 +5,6 @@ using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Image;
namespace Flow.Launcher
{
@ -60,6 +59,7 @@ namespace Flow.Launcher
Close();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
public async void Show(string title, string subTitle, string iconPath)
{
tbTitle.Text = title;

View file

@ -1,5 +1,4 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.IO;
@ -9,6 +8,8 @@ namespace Flow.Launcher
{
internal static class Notification
{
private static readonly string ClassName = nameof(Notification);
internal static bool legacy = !Win32Helper.IsNotificationSupported();
internal static void Uninstall()
@ -51,12 +52,12 @@ namespace Flow.Launcher
{
// Temporary fix for the Windows 11 notification issue
// Possibly from 22621.1413 or 22621.1485, judging by post time of #2024
Log.Exception("Flow.Launcher.Notification|Notification InvalidOperationException Error", e);
App.API.LogException(ClassName, "Notification InvalidOperationException Error", e);
LegacyShow(title, subTitle, iconPath);
}
catch (Exception e)
{
Log.Exception("Flow.Launcher.Notification|Notification Error", e);
App.API.LogException(ClassName, "Notification Error", e);
LegacyShow(title, subTitle, iconPath);
}
}

View file

@ -2,12 +2,13 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher
{
public partial class ProgressBoxEx : Window
{
private static readonly string ClassName = nameof(ProgressBoxEx);
private readonly Action _cancelProgress;
private ProgressBoxEx(Action cancelProgress)
@ -47,7 +48,7 @@ namespace Flow.Launcher
}
catch (Exception e)
{
Log.Error($"|ProgressBoxEx.Show|An error occurred: {e.Message}");
App.API.LogError(ClassName, $"An error occurred: {e.Message}");
await reportProgressAsync(null).ConfigureAwait(false);
}

View file

@ -38,20 +38,23 @@ namespace Flow.Launcher
public class PublicAPIInstance : IPublicAPI, IRemovable
{
private readonly Settings _settings;
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM;
// Must use getter to access Application.Current.Resources.MergedDictionaries so earlier
private Theme _theme;
private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>();
// Must use getter to avoid circular dependency
private Updater _updater;
private Updater Updater => _updater ??= Ioc.Default.GetRequiredService<Updater>();
private readonly object _saveSettingsLock = new();
#region Constructor
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
public PublicAPIInstance(Settings settings, MainViewModel mainVM)
{
_settings = settings;
_translater = translater;
_mainVM = mainVM;
GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
@ -100,8 +103,7 @@ namespace Flow.Launcher
remove => _mainVM.VisibilityChanged -= value;
}
// Must use Ioc.Default.GetRequiredService<Updater>() to avoid circular dependency
public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync(false);
public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false);
public void SaveAppAllSettings()
{
@ -178,7 +180,7 @@ namespace Flow.Launcher
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
public string GetTranslation(string key) => _translater.GetTranslation(key);
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
@ -433,16 +435,16 @@ namespace Flow.Launcher
PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") =>
Stopwatch.Debug($"|{className}.{methodName}|{message}", action);
Stopwatch.Debug(className, message, action, methodName);
public Task<long> StopwatchLogDebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "") =>
Stopwatch.DebugAsync($"|{className}.{methodName}|{message}", action);
Stopwatch.DebugAsync(className, message, action, methodName);
public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "") =>
Stopwatch.Normal($"|{className}.{methodName}|{message}", action);
Stopwatch.Info(className, message, action, methodName);
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "") =>
Stopwatch.NormalAsync($"|{className}.{methodName}|{message}", action);
Stopwatch.InfoAsync(className, message, action, methodName);
#endregion

View file

@ -8,8 +8,8 @@ using System.Windows;
using System.Windows.Documents;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher
{
@ -38,7 +38,7 @@ namespace Flow.Launcher
private void SetException(Exception exception)
{
string path = Log.CurrentLogDirectory;
var path = DataLocation.VersionLogDirectory;
var directory = new DirectoryInfo(path);
var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First();

View file

@ -0,0 +1,81 @@
<UserControl
x:Class="Flow.Launcher.Resources.Controls.InfoBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
d:DesignHeight="45"
d:DesignWidth="400"
mc:Ignorable="d">
<UserControl.Resources />
<Grid>
<Border
x:Name="PART_Border"
MinHeight="48"
Padding="18 18 18 18"
Background="{DynamicResource InfoBarInfoBG}"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1"
CornerRadius="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" MinWidth="24" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Border
x:Name="PART_IconBorder"
Width="16"
Height="16"
Margin="0 0 12 0"
VerticalAlignment="Top"
CornerRadius="10">
<ui:FontIcon
x:Name="PART_Icon"
Margin="1 0 0 1"
VerticalAlignment="Center"
FontFamily="Segoe MDL2 Assets"
FontSize="13"
Foreground="{DynamicResource Color01B}"
Visibility="Visible" />
</Border>
</StackPanel>
<StackPanel
x:Name="PART_StackPanel"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
x:Name="PART_Title"
Margin="0 0 12 0"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}"
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Title}" />
<TextBlock
x:Name="PART_Message"
Foreground="{DynamicResource Color05B}"
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Message}"
TextWrapping="Wrap" />
</StackPanel>
<Button
x:Name="PART_CloseButton"
Grid.Column="2"
Width="32"
Height="32"
VerticalAlignment="Center"
AutomationProperties.Name="Close InfoBar"
Click="PART_CloseButton_Click"
Content="&#xE10A;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
ToolTip="Close"
Visibility="Visible" />
</Grid>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,222 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Flow.Launcher.Resources.Controls
{
public partial class InfoBar : UserControl
{
public InfoBar()
{
InitializeComponent();
Loaded += InfoBar_Loaded;
}
private void InfoBar_Loaded(object sender, RoutedEventArgs e)
{
UpdateStyle();
UpdateTitleVisibility();
UpdateMessageVisibility();
UpdateOrientation();
UpdateIconAlignmentAndMargin();
UpdateIconVisibility();
UpdateCloseButtonVisibility();
}
public static readonly DependencyProperty TypeProperty =
DependencyProperty.Register(nameof(Type), typeof(InfoBarType), typeof(InfoBar), new PropertyMetadata(InfoBarType.Info, OnTypeChanged));
public InfoBarType Type
{
get => (InfoBarType)GetValue(TypeProperty);
set => SetValue(TypeProperty, value);
}
private static void OnTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateStyle();
}
}
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register(nameof(Message), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnMessageChanged));
public string Message
{
get => (string)GetValue(MessageProperty);
set
{
SetValue(MessageProperty, value);
}
}
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateMessageVisibility();
}
}
private void UpdateMessageVisibility()
{
PART_Message.Visibility = string.IsNullOrEmpty(Message) ? Visibility.Collapsed : Visibility.Visible;
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnTitleChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set
{
SetValue(TitleProperty, value);
UpdateTitleVisibility(); // Visibility update when change Title
}
}
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateTitleVisibility();
}
}
private void UpdateTitleVisibility()
{
PART_Title.Visibility = string.IsNullOrEmpty(Title) ? Visibility.Collapsed : Visibility.Visible;
}
public static readonly DependencyProperty IsIconVisibleProperty =
DependencyProperty.Register(nameof(IsIconVisible), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnIsIconVisibleChanged));
public bool IsIconVisible
{
get => (bool)GetValue(IsIconVisibleProperty);
set => SetValue(IsIconVisibleProperty, value);
}
public static readonly DependencyProperty LengthProperty =
DependencyProperty.Register(nameof(Length), typeof(InfoBarLength), typeof(InfoBar), new PropertyMetadata(InfoBarLength.Short, OnLengthChanged));
public InfoBarLength Length
{
get { return (InfoBarLength)GetValue(LengthProperty); }
set { SetValue(LengthProperty, value); }
}
private static void OnLengthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateOrientation();
infoBar.UpdateIconAlignmentAndMargin();
}
}
private void UpdateOrientation()
{
PART_StackPanel.Orientation = Length == InfoBarLength.Long ? Orientation.Vertical : Orientation.Horizontal;
}
private void UpdateIconAlignmentAndMargin()
{
if (Length == InfoBarLength.Short)
{
PART_IconBorder.VerticalAlignment = VerticalAlignment.Center;
PART_IconBorder.Margin = new Thickness(0, 0, 12, 0);
}
else
{
PART_IconBorder.VerticalAlignment = VerticalAlignment.Top;
PART_IconBorder.Margin = new Thickness(0, 2, 12, 0);
}
}
public static readonly DependencyProperty ClosableProperty =
DependencyProperty.Register(nameof(Closable), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnClosableChanged));
public bool Closable
{
get => (bool)GetValue(ClosableProperty);
set => SetValue(ClosableProperty, value);
}
private void PART_CloseButton_Click(object sender, RoutedEventArgs e)
{
Visibility = Visibility.Collapsed;
}
private void UpdateStyle()
{
switch (Type)
{
case InfoBarType.Info:
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
PART_Icon.Glyph = "\xF13F";
break;
case InfoBarType.Success:
PART_Border.Background = (Brush)FindResource("InfoBarSuccessBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarSuccessIcon");
PART_Icon.Glyph = "\xF13E";
break;
case InfoBarType.Warning:
PART_Border.Background = (Brush)FindResource("InfoBarWarningBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarWarningIcon");
PART_Icon.Glyph = "\xF13C";
break;
case InfoBarType.Error:
PART_Border.Background = (Brush)FindResource("InfoBarErrorBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarErrorIcon");
PART_Icon.Glyph = "\xF13D";
break;
default:
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
PART_Icon.Glyph = "\xF13F";
break;
}
}
private static void OnIsIconVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var infoBar = (InfoBar)d;
infoBar.UpdateIconVisibility();
}
private static void OnClosableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var infoBar = (InfoBar)d;
infoBar.UpdateCloseButtonVisibility();
}
private void UpdateIconVisibility()
{
PART_IconBorder.Visibility = IsIconVisible ? Visibility.Visible : Visibility.Collapsed;
}
private void UpdateCloseButtonVisibility()
{
PART_CloseButton.Visibility = Closable ? Visibility.Visible : Visibility.Collapsed;
}
}
public enum InfoBarType
{
Info,
Success,
Warning,
Error
}
public enum InfoBarLength
{
Short,
Long
}
}

View file

@ -106,7 +106,6 @@
<Color x:Key="NumberBoxColor24">#f5f5f5</Color>
<Color x:Key="NumberBoxColor25">#464646</Color>
<Color x:Key="NumberBoxColor26">#ffffff</Color>
<SolidColorBrush x:Key="NumberBoxPlaceHolder" Color="#565656" />
<Color x:Key="HoverStoreGrid">#272727</Color>
<!-- Resources for HotkeyControl -->
@ -115,10 +114,19 @@
<SolidColorBrush x:Key="CustomExpanderHover" Color="#323232" />
<!-- Resource for ContentDialog -->
<SolidColorBrush x:Key="ContentDialogOverlayBG" Color="#58000000" />
<!-- Infobar Warning -->
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#FCE100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<!-- DIY Infobar -->
<SolidColorBrush x:Key="InfoBarBD" Color="#19000000" />
<SolidColorBrush
x:Key="InfoBarInfoIcon"
Opacity="0.9"
Color="{m:DynamicColor SystemAccentColor}" />
<SolidColorBrush x:Key="InfoBarInfoBG" Color="#272727" />
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#fce100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<SolidColorBrush x:Key="InfoBarSuccessIcon" Color="#6ccb5f" />
<SolidColorBrush x:Key="InfoBarSuccessBG" Color="#393d1b" />
<SolidColorBrush x:Key="InfoBarErrorIcon" Color="#ff99a4" />
<SolidColorBrush x:Key="InfoBarErrorBG" Color="#442726" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />

View file

@ -108,10 +108,20 @@
<SolidColorBrush x:Key="CustomExpanderHover" Color="#f6f6f6" />
<!-- Resource for ContentDialog -->
<SolidColorBrush x:Key="ContentDialogOverlayBG" Color="#4D000000" />
<!-- Infobar Warning -->
<!-- DIY Infobar -->
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush
x:Key="InfoBarInfoIcon"
Opacity="0.8"
Color="{m:DynamicColor SystemAccentColor}" />
<SolidColorBrush x:Key="InfoBarInfoBG" Color="{m:DynamicColor Color01}" />
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#9D5D00" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#FFF4CE" />
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush x:Key="InfoBarSuccessIcon" Color="#3f973e" />
<SolidColorBrush x:Key="InfoBarSuccessBG" Color="#dff6dd" />
<SolidColorBrush x:Key="InfoBarErrorIcon" Color="#c42b1c" />
<SolidColorBrush x:Key="InfoBarErrorBG" Color="#fde7e9" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />

View file

@ -32,6 +32,8 @@
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->
<ListBox.Resources>
<converter:SizeRatioConverter x:Key="SizeRatioConverter" />
<converter:BadgePositionConverter x:Key="BadgePositionConverter" />
<converter:IconRadiusConverter x:Key="IconRadiusConverter" />
<converter:DiameterToCenterPointConverter x:Key="DiameterToCenterPointConverter" />
</ListBox.Resources>
@ -90,60 +92,64 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border
x:Name="Bullet"
Grid.Column="0"
Style="{DynamicResource BulletStyle}" />
<Border
Grid.Column="1"
Margin="9 0 0 0"
BorderBrush="Transparent"
BorderThickness="1">
<Image
x:Name="ImageIcon"
Margin="0 0 0 0"
HorizontalAlignment="Center"
IsHitTestVisible="False"
RenderOptions.BitmapScalingMode="Fant"
Source="{Binding Image, TargetNullValue={x:Null}}"
Stretch="Uniform"
StretchDirection="DownOnly"
Style="{DynamicResource ImageIconStyle}"
Visibility="{Binding ShowIcon}">
<Image.Clip>
<EllipseGeometry Center="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource DiameterToCenterPointConverter}}">
<EllipseGeometry.RadiusX>
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
<Binding ElementName="ImageIcon" Path="ActualWidth" />
<Binding Path="Result.RoundedIcon" />
</MultiBinding>
</EllipseGeometry.RadiusX>
<EllipseGeometry.RadiusY>
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
<Binding ElementName="ImageIcon" Path="ActualWidth" />
<Binding Path="Result.RoundedIcon" />
</MultiBinding>
</EllipseGeometry.RadiusY>
</EllipseGeometry>
</Image.Clip>
</Image>
</Border>
<Border
Grid.Column="1"
Margin="9 0 0 0"
BorderBrush="Transparent"
BorderThickness="0">
<TextBlock
x:Name="GlyphIcon"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{Binding Glyph.FontFamily}"
Style="{DynamicResource ItemGlyph}"
Text="{Binding Glyph.Glyph}"
Visibility="{Binding ShowGlyph}" />
<Grid>
<Image
x:Name="ImageIcon"
IsHitTestVisible="False"
RenderOptions.BitmapScalingMode="Fant"
Source="{Binding Image, TargetNullValue={x:Null}}"
Stretch="Uniform"
StretchDirection="DownOnly"
Style="{DynamicResource ImageIconStyle}"
Visibility="{Binding ShowIcon}">
<Image.Clip>
<EllipseGeometry Center="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource DiameterToCenterPointConverter}}">
<EllipseGeometry.RadiusX>
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
<Binding ElementName="ImageIcon" Path="ActualWidth" />
<Binding Path="Result.RoundedIcon" />
</MultiBinding>
</EllipseGeometry.RadiusX>
<EllipseGeometry.RadiusY>
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
<Binding ElementName="ImageIcon" Path="ActualWidth" />
<Binding Path="Result.RoundedIcon" />
</MultiBinding>
</EllipseGeometry.RadiusY>
</EllipseGeometry>
</Image.Clip>
</Image>
<TextBlock
x:Name="GlyphIcon"
FontFamily="{Binding Glyph.FontFamily}"
Style="{DynamicResource ItemGlyph}"
Text="{Binding Glyph.Glyph}"
Visibility="{Binding ShowGlyph}" />
<Image
x:Name="BadgeIcon"
Width="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource SizeRatioConverter}, ConverterParameter=0.6}"
Height="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource SizeRatioConverter}, ConverterParameter=0.6}"
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding BadgeImage, TargetNullValue={x:Null}}"
Visibility="{Binding ShowBadge}">
<Image.RenderTransform>
<TranslateTransform X="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource BadgePositionConverter}, ConverterParameter=1}" Y="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource BadgePositionConverter}, ConverterParameter=2}" />
</Image.RenderTransform>
</Image>
</Grid>
</Border>
</Grid>
<Grid

View file

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -179,6 +181,70 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
}
#region Korean IME
// The new Korean IME used in Windows 11 has compatibility issues with WPF. This issue is difficult to resolve within
// WPF itself, but it can be avoided by having the user switch to the legacy IME at the system level. Therefore,
// we provide guidance and a direct button for users to make this change themselves. If the relevant registry key does
// not exist (i.e., the Korean IME is not installed), this setting will not be shown at all.
public bool LegacyKoreanIMEEnabled
{
get => Win32Helper.IsLegacyKoreanIMEEnabled();
set
{
if (Win32Helper.SetLegacyKoreanIMEEnabled(value))
{
OnPropertyChanged();
OnPropertyChanged(nameof(KoreanIMERegistryValueIsZero));
}
else
{
//Since this is rarely seen text, language support is not provided.
App.API.ShowMsg("Failed to change Korean IME setting", "Please check your system registry access or contact support.");
}
}
}
public bool KoreanIMERegistryKeyExists
{
get
{
var registryKeyExists = Win32Helper.IsKoreanIMEExist();
var koreanLanguageInstalled = InputLanguage.InstalledInputLanguages.Cast<InputLanguage>().Any(lang => lang.Culture.Name.StartsWith("ko"));
var isWindows11 = Win32Helper.IsWindows11();
// Return true if Windows 11 with Korean IME installed, or if the registry key exists
return (isWindows11 && koreanLanguageInstalled) || registryKeyExists;
}
}
public bool KoreanIMERegistryValueIsZero
{
get
{
var value = Win32Helper.GetLegacyKoreanIMERegistryValue();
if (value is int intValue)
{
return intValue == 0;
}
else if (value != null && int.TryParse(value.ToString(), out var parsedValue))
{
return parsedValue == 0;
}
return false;
}
}
[RelayCommand]
private void OpenImeSettings()
{
Win32Helper.OpenImeSettings();
}
#endregion
public bool ShouldUsePinyin
{
get => Settings.ShouldUsePinyin;

View file

@ -5,7 +5,6 @@ using System.Windows.Controls;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
@ -112,10 +111,11 @@ public partial class SettingsPanePluginsViewModel : BaseModel
.ToList();
[RelayCommand]
private async Task OpenHelperAsync()
private async Task OpenHelperAsync(Button button)
{
var helpDialog = new ContentDialog()
{
Owner = Window.GetWindow(button),
Content = new StackPanel
{
Children =
@ -146,7 +146,6 @@ public partial class SettingsPanePluginsViewModel : BaseModel
}
}
},
PrimaryButtonText = (string)Application.Current.Resources["commonOK"],
CornerRadius = new CornerRadius(8),
Style = (Style)Application.Current.Resources["ContentDialog"]

View file

@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:converters="clr-namespace:Flow.Launcher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ext="clr-namespace:Flow.Launcher.Resources.MarkupExtensions"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -14,6 +15,9 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<ui:Page.Resources>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ui:Page.Resources>
<ScrollViewer
Margin="0"
CanContentScroll="False"
@ -28,6 +32,7 @@
Style="{StaticResource PageTitle}"
Text="{DynamicResource general}"
TextAlignment="left" />
<cc:ExCard
Title="{DynamicResource startFlowLauncherOnSystemStartup}"
Margin="0 8 0 0"
@ -215,12 +220,12 @@
<ui:NumberBox
Width="120"
Margin="0 0 0 0"
Minimum="0"
Maximum="1000"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{Binding SearchDelayTimeValue}"
ValidationMode="InvalidInputOverwritten" />
ValidationMode="InvalidInputOverwritten"
Value="{Binding SearchDelayTimeValue}" />
</StackPanel>
</cc:Card>
</cc:ExCard>
@ -312,6 +317,35 @@
SelectedValue="{Binding Language}"
SelectedValuePath="LanguageCode" />
</cc:Card>
<Border Visibility="{Binding KoreanIMERegistryKeyExists, Converter={StaticResource BoolToVisibilityConverter}}">
<cc:InfoBar
Title="{DynamicResource KoreanImeTitle}"
Margin="0 14 0 0"
Closable="False"
DataContext="{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=DataContext}"
IsIconVisible="True"
Length="Long"
Message="{DynamicResource KoreanImeGuide}"
Type="Warning"
Visibility="{Binding LegacyKoreanIMEEnabled, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverted, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</Border>
<cc:CardGroup Margin="0 14 0 0" Visibility="{Binding KoreanIMERegistryKeyExists, Converter={StaticResource BoolToVisibilityConverter}}">
<cc:Card
Title="{DynamicResource KoreanImeRegistry}"
Icon="&#xe88b;"
Sub="{DynamicResource KoreanImeRegistryTooltip}">
<ui:ToggleSwitch
IsOn="{Binding LegacyKoreanIMEEnabled}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource KoreanImeOpenLink}"
Icon="&#xF210;"
Sub="{DynamicResource KoreanImeOpenLinkToolTip}">
<Button Command="{Binding OpenImeSettingsCommand}" Content="{DynamicResource KoreanImeOpenLinkButton}" />
</cc:Card>
</cc:CardGroup>
</VirtualizingStackPanel>
</ScrollViewer>
</ui:Page>

View file

@ -57,6 +57,7 @@
Height="34"
Margin="0 0 20 0"
Command="{Binding OpenHelperCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Content="&#xe9ce;"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="14" />

View file

@ -698,11 +698,10 @@
</Grid>
</Border>
<!-- Fonts and icons -->
<cc:Card
Title="{DynamicResource useGlyphUI}"
Margin="0 0 0 0"
Margin="0 18 0 0"
Icon="&#xf6b8;"
Sub="{DynamicResource useGlyphUIEffect}">
<ui:ToggleSwitch
@ -710,6 +709,30 @@
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<!-- Badges -->
<cc:ExCard
Title="{DynamicResource showBadges}"
Margin="0 0 0 0"
Icon="&#xEC1B;"
Sub="{DynamicResource showBadgesToolTip}">
<cc:ExCard.SideContent>
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowBadges}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:ExCard.SideContent>
<cc:Card
Title="{DynamicResource showBadgesGlobalOnly}"
Sub="{DynamicResource showBadgesGlobalOnlyToolTip}"
Type="InsideFit">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowBadgesGlobalOnly}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
</cc:ExCard>
<!-- Settings color scheme -->
<cc:Card
Title="{DynamicResource ColorScheme}"

View file

@ -1,8 +1,8 @@
using System.Windows.Navigation;
using System.Windows.Navigation;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.SettingPages.ViewModels;
using Page = ModernWpf.Controls.Page;
using Flow.Launcher.Infrastructure.UserSettings;
using Page = ModernWpf.Controls.Page;
namespace Flow.Launcher.SettingPages.Views;

View file

@ -6,7 +6,6 @@ using System.Windows.Interop;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.SettingPages.Views;
using Flow.Launcher.ViewModel;
using ModernWpf.Controls;
@ -16,7 +15,6 @@ namespace Flow.Launcher;
public partial class SettingWindow
{
private readonly IPublicAPI _api;
private readonly Settings _settings;
private readonly SettingWindowViewModel _viewModel;
@ -26,7 +24,6 @@ public partial class SettingWindow
_settings = Ioc.Default.GetRequiredService<Settings>();
DataContext = viewModel;
_viewModel = viewModel;
_api = Ioc.Default.GetRequiredService<IPublicAPI>();
InitializePosition();
InitializeComponent();
}
@ -49,7 +46,7 @@ public partial class SettingWindow
_settings.SettingWindowTop = Top;
_settings.SettingWindowLeft = Left;
_viewModel.Save();
_api.SavePluginSettings();
App.API.SavePluginSettings();
}
private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)

View file

@ -38,7 +38,7 @@
BasedOn="{StaticResource BaseGlyphStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="Margin" Value="-4 0 0 0" />
<Setter Property="Margin" Value="0 0 0 0" />
</Style>
<Style
x:Key="QueryBoxStyle"
@ -180,7 +180,7 @@
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Margin" Value="-4 0 0 0" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="Width" Value="25" />
<Setter Property="Height" Value="25" />
<Setter Property="FontSize" Value="25" />

View file

@ -16,7 +16,6 @@ using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -30,6 +29,8 @@ namespace Flow.Launcher.ViewModel
{
#region Private Fields
private static readonly string ClassName = nameof(MainViewModel);
private bool _isQueryRunning;
private Query _lastQuery;
private string _queryTextBeforeLeaveResults;
@ -213,7 +214,7 @@ namespace Flow.Launcher.ViewModel
}
if (!_disposed)
Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends");
App.API.LogError(ClassName, "Unexpected ResultViewUpdate ends");
}
void continueAction(Task t)
@ -221,7 +222,7 @@ namespace Flow.Launcher.ViewModel
#if DEBUG
throw t.Exception;
#else
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
App.API.LogError(ClassName, $"Error happen in task dealing with viewupdate for results. {t.Exception}");
_resultsViewUpdateTask =
Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
#endif
@ -245,11 +246,19 @@ namespace Flow.Launcher.ViewModel
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
var resultsCopy = DeepCloneResults(e.Results, token);
foreach (var result in resultsCopy)
{
if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
result.BadgeIcoPath = pair.Metadata.IcoPath;
}
}
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
}
};
}
@ -883,7 +892,7 @@ namespace Flow.Launcher.ViewModel
#if DEBUG
throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value");
#else
Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible");
App.API.LogError(ClassName, "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible");
return false;
#endif
}
@ -1200,11 +1209,11 @@ namespace Flow.Launcher.ViewModel
_lastQuery = query;
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
if (string.IsNullOrEmpty(query.ActionKeyword))
{
// Wait 45 millisecond for query change in global query
// Wait 15 millisecond for query change in global query
// if query changes, return so that it won't be calculated
await Task.Delay(45, _updateSource.Token);
await Task.Delay(15, _updateSource.Token);
if (_updateSource.Token.IsCancellationRequested)
return;
}
@ -1268,8 +1277,7 @@ namespace Flow.Launcher.ViewModel
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
IReadOnlyList<Result> results =
await PluginManager.QueryForPluginAsync(plugin, query, token);
var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
if (token.IsCancellationRequested)
return;
@ -1285,10 +1293,18 @@ namespace Flow.Launcher.ViewModel
resultsCopy = DeepCloneResults(results, token);
}
foreach (var result in resultsCopy)
{
if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
result.BadgeIcoPath = plugin.Metadata.IcoPath;
}
}
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
token, reSelect)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
}
}
}
@ -1332,8 +1348,8 @@ namespace Flow.Launcher.ViewModel
}
catch (Exception e)
{
Log.Exception(
$"{nameof(MainViewModel)}.{nameof(ConstructQuery)}|Error when expanding shortcut {shortcut.Key}",
App.API.LogException(ClassName,
$"Error when expanding shortcut {shortcut.Key}",
e);
}
}

View file

@ -1,29 +0,0 @@
using System;
using System.Windows.Input;
namespace Flow.Launcher.ViewModel
{
public class RelayCommand : ICommand
{
private readonly Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
public virtual bool CanExecute(object parameter)
{
return true;
}
#pragma warning disable CS0067 // the event is never used
public event EventHandler CanExecuteChanged;
#pragma warning restore CS0067
public virtual void Execute(object parameter)
{
_action?.Invoke(parameter);
}
}
}

View file

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -14,6 +13,8 @@ namespace Flow.Launcher.ViewModel
{
public class ResultViewModel : BaseModel
{
private static readonly string ClassName = nameof(ResultViewModel);
private static readonly PrivateFontCollection FontCollection = new();
private static readonly Dictionary<string, string> Fonts = new();
@ -108,7 +109,6 @@ namespace Flow.Launcher.ViewModel
return IconXY;
}
}
public Visibility ShowGlyph
@ -124,10 +124,32 @@ namespace Flow.Launcher.ViewModel
}
}
public Visibility ShowBadge
{
get
{
// If results do not allow badges, or user has disabled badges in settings,
// or badge icon is not available, then do not show badge
if (!Result.ShowBadge || !Settings.ShowBadges || !BadgeIconAvailable)
return Visibility.Collapsed;
// If user has set to show badges only for global results, and this is not a global result,
// then do not show badge
if (Settings.ShowBadgesGlobalOnly && !IsGlobalQuery)
return Visibility.Collapsed;
return Visibility.Visible;
}
}
public bool IsGlobalQuery => string.IsNullOrEmpty(Result.OriginQuery.ActionKeyword);
private bool GlyphAvailable => Glyph is not null;
private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null;
private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null;
private bool PreviewImageAvailable => !string.IsNullOrEmpty(Result.Preview.PreviewImagePath) || Result.Preview.PreviewDelegate != null;
public string OpenResultModifiers => Settings.OpenResultModifiers;
@ -141,9 +163,11 @@ namespace Flow.Launcher.ViewModel
: Result.SubTitleToolTip;
private volatile bool _imageLoaded;
private volatile bool _badgeImageLoaded;
private volatile bool _previewImageLoaded;
private ImageSource _image = ImageLoader.LoadingImage;
private ImageSource _badgeImage = ImageLoader.LoadingImage;
private ImageSource _previewImage = ImageLoader.LoadingImage;
public ImageSource Image
@ -161,6 +185,21 @@ namespace Flow.Launcher.ViewModel
private set => _image = value;
}
public ImageSource BadgeImage
{
get
{
if (!_badgeImageLoaded)
{
_badgeImageLoaded = true;
_ = LoadBadgeImageAsync();
}
return _badgeImage;
}
private set => _badgeImage = value;
}
public ImageSource PreviewImage
{
get
@ -193,8 +232,8 @@ namespace Flow.Launcher.ViewModel
}
catch (Exception e)
{
Log.Exception(
$"|ResultViewModel.LoadImageInternalAsync|IcoPath is empty and exception when calling IconDelegate for result <{Result.Title}> of plugin <{Result.PluginDirectory}>",
App.API.LogException(ClassName,
$"IcoPath is empty and exception when calling IconDelegate for result <{Result.Title}> of plugin <{Result.PluginDirectory}>",
e);
}
}
@ -206,7 +245,7 @@ namespace Flow.Launcher.ViewModel
{
var imagePath = Result.IcoPath;
var iconDelegate = Result.Icon;
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
if (ImageLoader.TryGetValue(imagePath, false, out var img))
{
_image = img;
}
@ -217,11 +256,26 @@ namespace Flow.Launcher.ViewModel
}
}
private async Task LoadBadgeImageAsync()
{
var badgeImagePath = Result.BadgeIcoPath;
var badgeIconDelegate = Result.BadgeIcon;
if (ImageLoader.TryGetValue(badgeImagePath, false, out var img))
{
_badgeImage = img;
}
else
{
// We need to modify the property not field here to trigger the OnPropertyChanged event
BadgeImage = await LoadImageInternalAsync(badgeImagePath, badgeIconDelegate, false).ConfigureAwait(false);
}
}
private async Task LoadPreviewImageAsync()
{
var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath;
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
if (ImageLoader.TryGetValue(imagePath, true, out var img))
{
_previewImage = img;
}

View file

@ -19,7 +19,7 @@ namespace Flow.Launcher.ViewModel
public ResultCollection Results { get; }
private readonly object _collectionLock = new object();
private readonly object _collectionLock = new();
private readonly Settings _settings;
private int MaxResults => _settings?.MaxResultsToShow ?? 6;
@ -89,7 +89,7 @@ namespace Flow.Launcher.ViewModel
#region Private Methods
private int InsertIndexOf(int newScore, IList<ResultViewModel> list)
private static int InsertIndexOf(int newScore, IList<ResultViewModel> list)
{
int index = 0;
for (; index < list.Count; index++)
@ -118,7 +118,6 @@ namespace Flow.Launcher.ViewModel
}
}
#endregion
#region Public Methods
@ -190,10 +189,10 @@ namespace Flow.Launcher.ViewModel
if (token.IsCancellationRequested)
return;
UpdateResults(newResults, token, reselect);
UpdateResults(newResults, reselect, token);
}
private void UpdateResults(List<ResultViewModel> newResults, CancellationToken token = default, bool reselect = true)
private void UpdateResults(List<ResultViewModel> newResults, bool reselect = true, CancellationToken token = default)
{
lock (_collectionLock)
{

View file

@ -15,6 +15,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark;
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable
{
private static readonly string ClassName = nameof(Main);
internal static string _faviconCacheDir;
internal static PluginInitContext _context;
@ -221,7 +223,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
catch (Exception e)
{
var message = "Failed to set url in clipboard";
_context.API.LogException(nameof(Main), message, e);
_context.API.LogException(ClassName, message, e);
_context.API.ShowMsg(message);

View file

@ -15,6 +15,8 @@ namespace Flow.Launcher.Plugin.Explorer
{
internal class ContextMenu : IContextMenu
{
private static readonly string ClassName = nameof(ContextMenu);
private PluginInitContext Context { get; set; }
private Settings Settings { get; set; }
@ -469,7 +471,7 @@ namespace Flow.Launcher.Plugin.Explorer
private void LogException(string message, Exception e)
{
Context.API.LogException(nameof(ContextMenu), message, e);
Context.API.LogException(ClassName, message, e);
}
private static bool CanRunAsDifferentUser(string path)

View file

@ -45,6 +45,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<!-- Do not upgrade System.Data.OleDb since we are .Net7.0 -->
<PackageReference Include="System.Data.OleDb" Version="8.0.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
@ -341,7 +341,7 @@ namespace Peter
return null;
}
IShellFolder oParentFolder = GetParentFolder(arrFI[0].Parent.FullName);
IShellFolder oParentFolder = GetParentFolder(arrFI[0].Parent!.FullName);
if (null == oParentFolder)
{
return null;
@ -1535,7 +1535,7 @@ namespace Peter
m_hookType,
m_filterFunc,
IntPtr.Zero,
(int)AppDomain.GetCurrentThreadId());
Environment.CurrentManagedThreadId);
}
// ************************************************************************

View file

@ -16,7 +16,7 @@ public static class SortOptionTranslationHelper
ArgumentNullException.ThrowIfNull(API);
var enumName = Enum.GetName(sortOption);
var splited = enumName.Split('_');
var splited = enumName!.Split('_');
var name = string.Join('_', splited[..^1]);
var direction = splited[^1];

View file

@ -9,6 +9,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
public static class DirectoryInfoSearch
{
private static readonly string ClassName = nameof(DirectoryInfoSearch);
internal static IEnumerable<SearchResult> TopLevelDirectorySearch(Query query, string search, CancellationToken token)
{
var criteria = ConstructSearchCriteria(search);
@ -75,7 +77,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
}
catch (Exception e)
{
Main.Context.API.LogException(nameof(DirectoryInfoSearch), "Error occurred while searching path", e);
Main.Context.API.LogException(ClassName, "Error occurred while searching path", e);
throw;
}

View file

@ -47,7 +47,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
foreach (DictionaryEntry special in Environment.GetEnvironmentVariables())
{
var path = special.Value.ToString();
var path = special.Value!.ToString();
// we add a trailing slash to the path to make sure drive paths become valid absolute paths.
// for example, if %systemdrive% is C: we turn it to C:\
path = path.EnsureTrailingSlash();
@ -61,7 +61,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
// Variables are returned with a mixture of all upper/lower case.
// Call ToUpper() to make the results look consistent
_envStringPaths.Add(special.Key.ToString().ToUpper(), path);
_envStringPaths.Add(special.Key.ToString()!.ToUpper(), path);
}
}
}

View file

@ -6,16 +6,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class QueryConstructor
{
private static Regex _specialCharacterMatcher = new(@"[\@\\#\\&\_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
private static Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
private static readonly Regex _specialCharacterMatcher = new(@"[\@\\#\\&\_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
private Settings settings { get; }
private Settings Settings { get; }
private const string SystemIndex = "SystemIndex";
public QueryConstructor(Settings settings)
{
this.settings = settings;
Settings = settings;
}
public CSearchQueryHelper CreateBaseQuery()
@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
var baseQuery = CreateQueryHelper();
// Set the number of results we want. Don't set this property if all results are needed.
baseQuery.QueryMaxResults = settings.MaxResult;
baseQuery.QueryMaxResults = Settings.MaxResult;
// Set list of columns we want to display, getting the path presently
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
@ -37,7 +37,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return baseQuery;
}
internal CSearchQueryHelper CreateQueryHelper()
internal static CSearchQueryHelper CreateQueryHelper()
{
// This uses the Microsoft.Search.Interop assembly
// Throws COMException if Windows Search service is not running/disabled, this needs to be caught
@ -54,20 +54,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public static string TopLevelDirectoryConstraint(ReadOnlySpan<char> path) => $"directory='file:{path}'";
public static string RecursiveDirectoryConstraint(ReadOnlySpan<char> path) => $"scope='file:{path}'";
///<summary>
/// Search will be performed on all folders and files on the first level of a specified directory.
///</summary>
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
{
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND ({FileName} LIKE '{searchString}%' OR CONTAINS({FileName},'\"{searchString}*\"'))";
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
var scopeConstraint = recursive
? RecursiveDirectoryConstraint(path)
: TopLevelDirectoryConstraint(path);
var query = $"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {FileName}";
var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
return query;
}
@ -84,7 +83,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
}
/// <summary>
@ -121,10 +120,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
/// <summary>
/// Order identifier: file name
/// Order identifier: System.Search.Rank DESC
/// </summary>
public const string FileName = "System.FileName";
/// <remarks>
/// <see href="https://docs.microsoft.com/en-us/windows/win32/properties/props-system-search-rank"/>
/// </remarks>
public const string OrderIdentifier = "System.Search.Rank DESC";
///<summary>
/// Search will be performed on all indexed file contents for the specified search keywords.
@ -132,7 +133,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
public string FileContent(ReadOnlySpan<char> userSearchString)
{
string query =
$"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
$"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
return query;
}

View file

@ -82,14 +82,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return HandledEngineNotAvailableExceptionAsync();
}
}
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token)
{
return WindowsIndexFilesAndFoldersSearchAsync(search, token: token);
}
public IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
{
return WindowsIndexFileContentSearchAsync(contentSearch, token);
}
public IAsyncEnumerable<SearchResult> EnumerateAsync(string path, string search, bool recursive, CancellationToken token)
{
return WindowsIndexTopLevelFolderSearchAsync(search, path, recursive, token);
@ -100,19 +103,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
if (!Settings.WarnWindowsSearchServiceOff)
return AsyncEnumerable.Empty<SearchResult>();
var api = Main.Context.API;
throw new EngineNotAvailableException(
"Windows Index",
api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
Constants.WindowsIndexErrorImagePath,
c =>
{
Settings.WarnWindowsSearchServiceOff = false;
// Clears the warning message so user is not mistaken that it has not worked
api.ChangeQuery(string.Empty);
Main.Context.API.ChangeQuery(string.Empty);
return ValueTask.FromResult(false);
});

View file

@ -1,13 +1,15 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
#nullable enable
namespace Flow.Launcher.Plugin.Explorer.Views
{
public class ActionKeywordModel : INotifyPropertyChanged
{
private static Settings _settings;
private static Settings _settings = null!;
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public static void Init(Settings settings)
{
@ -54,4 +56,4 @@ namespace Flow.Launcher.Plugin.Explorer.Views
}
}
}
}
}

View file

@ -1,27 +0,0 @@
using System;
using System.Windows.Input;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
internal class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
public virtual bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public virtual void Execute(object parameter)
{
_action?.Invoke(parameter);
}
}
}

View file

@ -1,9 +1,4 @@
#nullable enable
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Views;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -13,11 +8,16 @@ using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Views;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
public class SettingsViewModel : BaseModel
public partial class SettingsViewModel : BaseModel
{
public Settings Settings { get; set; }
@ -36,7 +36,6 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
InitializeActionKeywordModels();
}
public void Save()
{
Context.API.SaveSettingJsonStorage<Settings>();
@ -48,7 +47,6 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
private EnumBindingModel<Settings.ContentIndexSearchEngineOption> _selectedContentSearchEngine;
private EnumBindingModel<Settings.PathEnumerationEngineOption> _selectedPathEnumerationEngine;
public EnumBindingModel<Settings.IndexSearchEngineOption> SelectedIndexSearchEngine
{
get => _selectedIndexSearchEngine;
@ -261,8 +259,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
public ActionKeywordModel? SelectedActionKeyword { get; set; }
public ICommand EditActionKeywordCommand => new RelayCommand(EditActionKeyword);
[RelayCommand]
private void EditActionKeyword(object obj)
{
if (SelectedActionKeyword is not { } actionKeyword)
@ -307,12 +304,6 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
public AccessLink? SelectedQuickAccessLink { get; set; }
public AccessLink? SelectedIndexSearchExcludedPath { get; set; }
public ICommand RemoveLinkCommand => new RelayCommand(RemoveLink);
public ICommand EditLinkCommand => new RelayCommand(EditLink);
public ICommand AddLinkCommand => new RelayCommand(AddLink);
public void AppendLink(string containerName, AccessLink link)
{
var container = containerName switch
@ -324,6 +315,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
container.Add(link);
}
[RelayCommand]
private void EditLink(object commandParameter)
{
var (selectedLink, collection) = commandParameter switch
@ -360,7 +352,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Context.API.ShowMsgBox(warning);
}
[RelayCommand]
private void AddLink(object commandParameter)
{
var container = commandParameter switch
@ -385,6 +377,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
container.Add(newAccessLink);
}
[RelayCommand]
private void RemoveLink(object obj)
{
if (obj is not string container) return;
@ -435,7 +428,6 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
return path;
}
internal static void OpenWindowsIndexingOptions()
{
var psi = new ProcessStartInfo
@ -448,39 +440,35 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi);
}
private ICommand? _openFileEditorPathCommand;
public ICommand OpenFileEditorPath => _openFileEditorPathCommand ??= new RelayCommand(_ =>
[RelayCommand]
private void OpenFileEditorPath()
{
var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null);
if (path is null)
return;
FileEditorPath = path;
});
}
private ICommand? _openFolderEditorPathCommand;
public ICommand OpenFolderEditorPath => _openFolderEditorPathCommand ??= new RelayCommand(_ =>
[RelayCommand]
private void OpenFolderEditorPath()
{
var path = PromptUserSelectPath(ResultType.File, Settings.FolderEditorPath != null ? Path.GetDirectoryName(Settings.FolderEditorPath) : null);
if (path is null)
return;
FolderEditorPath = path;
});
}
private ICommand? _openShellPathCommand;
public ICommand OpenShellPath => _openShellPathCommand ??= new RelayCommand(_ =>
[RelayCommand]
private void OpenShellPath()
{
var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null);
if (path is null)
return;
ShellPath = path;
});
}
public string FileEditorPath
{
@ -537,7 +525,6 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
}
}
#region Everything FastSortWarning
public Visibility FastSortWarningVisibility
@ -593,7 +580,5 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
}
#endregion
}
}

View file

@ -86,6 +86,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
DialogResult = false;
Close();
}
private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
@ -99,6 +100,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
e.Handled = true;
}
}
private void TextBox_Pasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
@ -114,12 +116,13 @@ namespace Flow.Launcher.Plugin.Explorer.Views
e.CancelCommand();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))

View file

@ -245,7 +245,7 @@
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Command="{Binding OpenFileEditorPath}"
Command="{Binding OpenFileEditorPathCommand}"
Content="{DynamicResource select}" />
</StackPanel>
@ -272,7 +272,7 @@
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Command="{Binding OpenFolderEditorPath}"
Command="{Binding OpenFolderEditorPathCommand}"
Content="{DynamicResource select}" />
</StackPanel>
@ -299,7 +299,7 @@
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Command="{Binding OpenShellPath}"
Command="{Binding OpenShellPathCommand}"
Content="{DynamicResource select}" />
</StackPanel>
@ -598,7 +598,7 @@
<ComboBox
Grid.Row="2"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
VerticalAlignment="Center"
ItemsSource="{Binding Settings.SortOptions, Mode=OneWay}"
SelectedItem="{Binding Settings.SortOption}"
@ -623,7 +623,7 @@
Grid.Row="3"
Grid.Column="1"
Width="{StaticResource SettingPanelPathTextBoxWidth}"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
HorizontalAlignment="Left"
Text="{Binding EverythingInstalledPath}" />

View file

@ -1,11 +1,10 @@
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using DataFormats = System.Windows.DataFormats;
using DragDropEffects = System.Windows.DragDropEffects;
using DragEventArgs = System.Windows.DragEventArgs;
@ -19,9 +18,6 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
private readonly SettingsViewModel viewModel;
private List<ActionKeywordModel> actionKeywordsListView;
public ExplorerSettings(SettingsViewModel viewModel)
{
DataContext = viewModel;
@ -39,8 +35,6 @@ namespace Flow.Launcher.Plugin.Explorer.Views
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
private void AccessLinkDragDrop(string containerName, DragEventArgs e)
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);

View file

@ -10,6 +10,7 @@
<system:String x:Key="flowlauncher_plugin_processkiller_kill_all_count">kill {0} processes</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_kill_instances">kill all instances</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_show_window_title">Show title for processes with visible windows</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_put_visible_window_process_top">Put processes with visible windows on the top</system:String>
</ResourceDictionary>

View file

@ -81,7 +81,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller
// Filter processes based on search term
var searchTerm = query.Search;
var processlist = new List<ProcessResult>();
var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();
var processWindowTitle =
Settings.ShowWindowTitle || Settings.PutVisibleWindowProcessesTop ?
ProcessHelper.GetProcessesWithNonEmptyWindowTitle() :
new Dictionary<int, string>();
if (string.IsNullOrWhiteSpace(searchTerm))
{
foreach (var p in allPocessList)
@ -91,12 +94,22 @@ namespace Flow.Launcher.Plugin.ProcessKiller
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
{
// Add score to prioritize processes with visible windows
// And use window title for those processes
processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle));
// Use window title for those processes if enabled
processlist.Add(new ProcessResult(
p,
Settings.PutVisibleWindowProcessesTop ? 200 : 0,
Settings.ShowWindowTitle ? windowTitle : progressNameIdTitle,
null,
progressNameIdTitle));
}
else
{
processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle));
processlist.Add(new ProcessResult(
p,
0,
progressNameIdTitle,
null,
progressNameIdTitle));
}
}
}
@ -115,13 +128,17 @@ namespace Flow.Launcher.Plugin.ProcessKiller
if (score > 0)
{
// Add score to prioritize processes with visible windows
// And use window title for those processes
// Use window title for those processes
if (Settings.PutVisibleWindowProcessesTop)
{
score += 200;
}
processlist.Add(new ProcessResult(p, score, windowTitle,
score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle));
processlist.Add(new ProcessResult(
p,
score,
Settings.ShowWindowTitle ? windowTitle : progressNameIdTitle,
score == windowTitleMatch.Score ? windowTitleMatch : null,
progressNameIdTitle));
}
}
else
@ -130,7 +147,12 @@ namespace Flow.Launcher.Plugin.ProcessKiller
var score = processNameIdMatch.Score;
if (score > 0)
{
processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle));
processlist.Add(new ProcessResult(
p,
score,
progressNameIdTitle,
processNameIdMatch,
progressNameIdTitle));
}
}
}

View file

@ -1,9 +1,11 @@
using Microsoft.Win32.SafeHandles;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;
@ -12,6 +14,8 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessHelper
{
private static readonly string ClassName = nameof(ProcessHelper);
private readonly HashSet<string> _systemProcessList = new()
{
"conhost",
@ -70,8 +74,21 @@ namespace Flow.Launcher.Plugin.ProcessKiller
/// </summary>
public static unsafe Dictionary<int, string> GetProcessesWithNonEmptyWindowTitle()
{
var processDict = new Dictionary<int, string>();
// Collect all window handles
var windowHandles = new List<HWND>();
PInvoke.EnumWindows((hWnd, _) =>
{
if (PInvoke.IsWindowVisible(hWnd))
{
windowHandles.Add(hWnd);
}
return true;
}, IntPtr.Zero);
// Concurrently process each window handle
var processDict = new ConcurrentDictionary<int, string>();
var processedProcessIds = new ConcurrentDictionary<int, byte>();
Parallel.ForEach(windowHandles, hWnd =>
{
var windowTitle = GetWindowTitle(hWnd);
if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd))
@ -80,20 +97,26 @@ namespace Flow.Launcher.Plugin.ProcessKiller
var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId);
if (result == 0u || processId == 0u)
{
return false;
return;
}
var process = Process.GetProcessById((int)processId);
if (!processDict.ContainsKey((int)processId))
// Ensure each process ID is processed only once
if (processedProcessIds.TryAdd((int)processId, 0))
{
processDict.Add((int)processId, windowTitle);
try
{
var process = Process.GetProcessById((int)processId);
processDict.TryAdd((int)processId, windowTitle);
}
catch
{
// Handle exceptions (e.g., process exited)
}
}
}
});
return true;
}, IntPtr.Zero);
return processDict;
return new Dictionary<int, string>(processDict);
}
private static unsafe string GetWindowTitle(HWND hwnd)
@ -131,7 +154,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
}
catch (Exception e)
{
context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
context.API.LogException(ClassName, $"Failed to kill process {p.ProcessName}", e);
}
}

View file

@ -2,6 +2,8 @@
{
public class Settings
{
public bool ShowWindowTitle { get; set; } = true;
public bool PutVisibleWindowProcessesTop { get; set; } = false;
}
}

View file

@ -9,6 +9,12 @@
Settings = settings;
}
public bool ShowWindowTitle
{
get => Settings.ShowWindowTitle;
set => Settings.ShowWindowTitle = value;
}
public bool PutVisibleWindowProcessesTop
{
get => Settings.PutVisibleWindowProcessesTop;

View file

@ -12,10 +12,16 @@
<Grid.ColumnDefinitions />
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<CheckBox
Grid.Row="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_processkiller_show_window_title}"
IsChecked="{Binding ShowWindowTitle}" />
<CheckBox
Grid.Row="1"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_processkiller_put_visible_window_process_top}"
IsChecked="{Binding PutVisibleWindowProcessesTop}" />
</Grid>

View file

@ -109,7 +109,6 @@ namespace Flow.Launcher.Plugin.Program
}
catch (OperationCanceledException)
{
Context.API.LogDebug(ClassName, "Query operation cancelled");
return emptyResults;
}
finally

View file

@ -146,6 +146,7 @@ namespace Flow.Launcher.Plugin.Program.Views
programSourceView.Items.Refresh();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void ReIndexing()
{
ViewRefresh();
@ -183,6 +184,7 @@ namespace Flow.Launcher.Plugin.Program.Views
EditProgramSource(selectedProgramSource);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void EditProgramSource(ProgramSource selectedProgramSource)
{
if (selectedProgramSource == null)
@ -277,6 +279,7 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
{
await ProgramSettingDisplay.DisplayAllProgramsAsync();
@ -284,6 +287,7 @@ namespace Flow.Launcher.Plugin.Program.Views
ViewRefresh();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e)
{
var selectedItems = programSourceView

View file

@ -362,6 +362,7 @@ namespace Flow.Launcher.Plugin.Sys
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe89f"),
Action = c =>
{
_context.API.HideMainWindow();
Application.Current.MainWindow.Close();
return true;
}

View file

@ -30,6 +30,7 @@ namespace Flow.Launcher.Plugin.WebSearch
Initialize(sources, context, Action.Add);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void Initialize(IList<SearchSource> sources, PluginInitContext context, Action action)
{
InitializeComponent();
@ -126,6 +127,7 @@ namespace Flow.Launcher.Plugin.WebSearch
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void OnSelectIconClick(object sender, RoutedEventArgs e)
{
const string filter = "Image files (*.jpg, *.jpeg, *.gif, *.png, *.bmp) |*.jpg; *.jpeg; *.gif; *.png; *.bmp";

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public class Baidu : SuggestionSource
{
private static readonly string ClassName = nameof(Baidu);
private readonly Regex _reg = new Regex("window.baidu.sug\\((.*)\\)");
public override async Task<List<string>> SuggestionsAsync(string query, CancellationToken token)
@ -24,7 +26,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Main._context.API.LogException(nameof(Baidu), "Can't get suggestion from Baidu", e);
Main._context.API.LogException(ClassName, "Can't get suggestion from Baidu", e);
return null;
}
@ -39,7 +41,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (JsonException e)
{
Main._context.API.LogException(nameof(Baidu), "Can't parse suggestions", e);
Main._context.API.LogException(ClassName, "Can't parse suggestions", e);
return new List<string>();
}

View file

@ -10,6 +10,8 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public class Bing : SuggestionSource
{
private static readonly string ClassName = nameof(Bing);
public override async Task<List<string>> SuggestionsAsync(string query, CancellationToken token)
{
try
@ -33,12 +35,12 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or { InnerException: TimeoutException })
{
Main._context.API.LogException(nameof(Bing), "Can't get suggestion from Bing", e);
Main._context.API.LogException(ClassName, "Can't get suggestion from Bing", e);
return null;
}
catch (JsonException e)
{
Main._context.API.LogException(nameof(Bing), "Can't parse suggestions", e);
Main._context.API.LogException(ClassName, "Can't parse suggestions", e);
return new List<string>();
}
}

View file

@ -10,6 +10,8 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public class DuckDuckGo : SuggestionSource
{
private static readonly string ClassName = nameof(DuckDuckGo);
public override async Task<List<string>> SuggestionsAsync(string query, CancellationToken token)
{
// When the search query is empty, DuckDuckGo returns `[]`. When it's not empty, it returns data
@ -34,12 +36,12 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Main._context.API.LogException(nameof(DuckDuckGo), "Can't get suggestion from DuckDuckGo", e);
Main._context.API.LogException(ClassName, "Can't get suggestion from DuckDuckGo", e);
return null;
}
catch (JsonException e)
{
Main._context.API.LogException(nameof(DuckDuckGo), "Can't parse suggestions", e);
Main._context.API.LogException(ClassName, "Can't parse suggestions", e);
return new List<string>();
}
}

View file

@ -10,6 +10,8 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public class Google : SuggestionSource
{
private static readonly string ClassName = nameof(Google);
public override async Task<List<string>> SuggestionsAsync(string query, CancellationToken token)
{
try
@ -27,12 +29,12 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
}
catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException})
{
Main._context.API.LogException(nameof(Google), "Can't get suggestion from Google", e);
Main._context.API.LogException(ClassName, "Can't get suggestion from Google", e);
return null;
}
catch (JsonException e)
{
Main._context.API.LogException(nameof(Google), "Can't parse suggestions", e);
Main._context.API.LogException(ClassName, "Can't parse suggestions", e);
return new List<string>();
}
}

View file

@ -11,10 +11,12 @@ namespace Flow.Launcher.Plugin.WindowsSettings
{
_api = api;
}
public static void Exception(string message, Exception exception, Type type, [CallerMemberName] string methodName = "")
{
_api?.LogException(type.FullName, message, exception, methodName);
}
public static void Warn(string message, Type type, [CallerMemberName] string methodName = "")
{
_api?.LogWarn(type.FullName, message, methodName);

View file

@ -351,6 +351,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
</p>
<p align="center">
<a href="https://github.com/itsonlyfrans"><img src="https://avatars.githubusercontent.com/u/46535667?v=4" width="10%" /></a>
<a href="https://github.com/atilford"><img src="https://avatars.githubusercontent.com/u/13649625?v=4" width="10%" /></a>
<a href="https://github.com/andreqramos"><img src="https://avatars.githubusercontent.com/u/49326063?v=4" width="10%" /></a>
<a href="https://github.com/Yuba4"><img src="https://avatars.githubusercontent.com/u/46278200?v=4" width="10%" /></a>
<a href="https://github.com/Mavrik327"><img src="https://avatars.githubusercontent.com/u/121626149?v=4" width="10%" /></a>